From a6139aa0035d19d794a3669d6196f9f32a8c8352 Mon Sep 17 00:00:00 2001 From: easong-openai Date: Mon, 4 Aug 2025 10:42:39 -0700 Subject: [PATCH 0001/1309] Update prompt.md (#1819) The existing prompt is really bad. As a low-hanging fruit, let's correct the apply_patch instructions - this helps smaller models successfully apply patches. --- codex-rs/core/prompt.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/codex-rs/core/prompt.md b/codex-rs/core/prompt.md index 0a4578270a..4e55003b9f 100644 --- a/codex-rs/core/prompt.md +++ b/codex-rs/core/prompt.md @@ -10,7 +10,7 @@ You MUST adhere to the following criteria when executing the task: - Showing user code and tool call details is allowed. - User instructions may overwrite the _CODING GUIDELINES_ section in this developer message. - Do not use \`ls -R\`, \`find\`, or \`grep\` - these are slow in large repos. Use \`rg\` and \`rg --files\`. -- Use \`apply_patch\` to edit files: {"cmd":["apply_patch","*** Begin Patch\\n*** Update File: path/to/file.py\\n@@ def example():\\n- pass\\n+ return 123\\n*** End Patch"]} +- Use \`apply_patch\` to edit files: {"command":["apply_patch","*** Begin Patch\\n*** Update File: path/to/file.py\\n@@ def example():\\n- pass\\n+ return 123\\n*** End Patch"]} - If completing the user's task requires writing or modifying files: - Your code and final answer should follow these _CODING GUIDELINES_: - Fix the problem at the root cause rather than applying surface-level patches, when possible. @@ -40,16 +40,16 @@ You MUST adhere to the following criteria when executing the task: Your patch language is a stripped‑down, file‑oriented diff format designed to be easy to parse and safe to apply. You can think of it as a high‑level envelope: -**_ Begin Patch +*** Begin Patch [ one or more file sections ] -_** End Patch +*** End Patch Within that envelope, you get a sequence of file operations. You MUST include a header to specify the action you are taking. Each operation starts with one of three headers: -**_ Add File: - create a new file. Every following line is a + line (the initial contents). -_** Delete File: - remove an existing file. Nothing follows. +*** Add File: - create a new file. Every following line is a + line (the initial contents). +*** Delete File: - remove an existing file. Nothing follows. \*\*\* Update File: - patch an existing file in place (optionally with a rename). May be immediately followed by \*\*\* Move to: if you want to rename the file. @@ -63,28 +63,28 @@ Within a hunk each line starts with: At the end of a truncated hunk you can emit \*\*\* End of File. Patch := Begin { FileOp } End -Begin := "**_ Begin Patch" NEWLINE -End := "_** End Patch" NEWLINE +Begin := "*** Begin Patch" NEWLINE +End := "*** End Patch" NEWLINE FileOp := AddFile | DeleteFile | UpdateFile -AddFile := "**_ Add File: " path NEWLINE { "+" line NEWLINE } -DeleteFile := "_** Delete File: " path NEWLINE -UpdateFile := "**_ Update File: " path NEWLINE [ MoveTo ] { Hunk } -MoveTo := "_** Move to: " newPath NEWLINE +AddFile := "*** Add File: " path NEWLINE { "+" line NEWLINE } +DeleteFile := "*** Delete File: " path NEWLINE +UpdateFile := "*** Update File: " path NEWLINE [ MoveTo ] { Hunk } +MoveTo := "*** Move to: " newPath NEWLINE Hunk := "@@" [ header ] NEWLINE { HunkLine } [ "*** End of File" NEWLINE ] HunkLine := (" " | "-" | "+") text NEWLINE A full patch can combine several operations: -**_ Begin Patch -_** Add File: hello.txt +*** Begin Patch +*** Add File: hello.txt +Hello world -**_ Update File: src/app.py -_** Move to: src/main.py +*** Update File: src/app.py +*** Move to: src/main.py @@ def greet(): -print("Hi") +print("Hello, world!") -**_ Delete File: obsolete.txt -_** End Patch +*** Delete File: obsolete.txt +*** End Patch It is important to remember: @@ -101,7 +101,7 @@ Plan updates A tool named `update_plan` is available. Use it to keep an up‑to‑date, step‑by‑step plan for the task so you can follow your progress. When making your plans, keep in mind that you are a deployed coding agent - `update_plan` calls should not involve doing anything that you aren't capable of doing. For example, `update_plan` calls should NEVER contain tasks to merge your own pull requests. Only stop to ask the user if you genuinely need their feedback on a change. -- At the start of the task, call `update_plan` with an initial plan: a short list of 1‑sentence steps with a `status` for each step (`pending`, `in_progress`, or `completed`). There should always be exactly one `in_progress` step until everything is done. +- At the start of any nontrivial task, call `update_plan` with an initial plan: a short list of 1‑sentence steps with a `status` for each step (`pending`, `in_progress`, or `completed`). There should always be exactly one `in_progress` step until everything is done. - Whenever you finish a step, call `update_plan` again, marking the finished step as `completed` and the next step as `in_progress`. - If your plan needs to change, call `update_plan` with the revised steps and include an `explanation` describing the change. - When all steps are complete, make a final `update_plan` call with all steps marked `completed`. From 64cfbbd3c8609fb1c1a1bfbc1bf86148e12e6cc4 Mon Sep 17 00:00:00 2001 From: Jeremy Rose <172423086+nornagon-openai@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:25:01 -0700 Subject: [PATCH 0002/1309] support more keys in textarea (#1820) Added: * C-m for newline (not sure if this is actually treated differently to Enter, but tui-textarea handles it and it doesn't hurt) * C-d to delete one char forwards (same as Del) * A-bksp to delete backwards one word * A-arrows to navigate by word --- codex-rs/tui/src/bottom_pane/textarea.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/textarea.rs b/codex-rs/tui/src/bottom_pane/textarea.rs index e150135b75..cb30c2ac7a 100644 --- a/codex-rs/tui/src/bottom_pane/textarea.rs +++ b/codex-rs/tui/src/bottom_pane/textarea.rs @@ -210,7 +210,7 @@ impl TextArea { .. } => self.insert_str(&c.to_string()), KeyEvent { - code: KeyCode::Char('j'), + code: KeyCode::Char('j' | 'm'), modifiers: KeyModifiers::CONTROL, .. } @@ -220,11 +220,22 @@ impl TextArea { } => self.insert_str("\n"), KeyEvent { code: KeyCode::Backspace, + modifiers: KeyModifiers::ALT, + .. + } => self.delete_backward_word(), + KeyEvent { + code: KeyCode::Backspace, + modifiers: KeyModifiers::NONE, .. } => self.delete_backward(1), KeyEvent { code: KeyCode::Delete, .. + } + | KeyEvent { + code: KeyCode::Char('d'), + modifiers: KeyModifiers::CONTROL, + .. } => self.delete_forward(1), KeyEvent { @@ -303,14 +314,14 @@ impl TextArea { } KeyEvent { code: KeyCode::Left, - modifiers: KeyModifiers::CONTROL, + modifiers: KeyModifiers::CONTROL | KeyModifiers::ALT, .. } => { self.set_cursor(self.beginning_of_previous_word()); } KeyEvent { code: KeyCode::Right, - modifiers: KeyModifiers::CONTROL, + modifiers: KeyModifiers::CONTROL | KeyModifiers::ALT, .. } => { self.set_cursor(self.end_of_next_word()); From 2899817c94098caf96009c1d797597df1c298e3f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 14:24:19 -0700 Subject: [PATCH 0003/1309] chore(deps): bump toml from 0.9.2 to 0.9.4 in /codex-rs (#1815) Bumps [toml](https://github.com/toml-rs/toml) from 0.9.2 to 0.9.4.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=toml&package-manager=cargo&previous-version=0.9.2&new-version=0.9.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- codex-rs/Cargo.lock | 10 +++++----- codex-rs/core/Cargo.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index eb4eccd897..9d8a027c53 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -661,7 +661,7 @@ dependencies = [ "clap", "codex-core", "serde", - "toml 0.9.2", + "toml 0.9.4", ] [[package]] @@ -707,7 +707,7 @@ dependencies = [ "tokio", "tokio-test", "tokio-util", - "toml 0.9.2", + "toml 0.9.4", "tracing", "tree-sitter", "tree-sitter-bash", @@ -831,7 +831,7 @@ dependencies = [ "tempfile", "tokio", "tokio-test", - "toml 0.9.2", + "toml 0.9.4", "tracing", "tracing-subscriber", "uuid", @@ -4773,9 +4773,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac" +checksum = "41ae868b5a0f67631c14589f7e250c1ea2c574ee5ba21c6c8dd4b1485705a5a1" dependencies = [ "indexmap 2.10.0", "serde", diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 466e9adf02..e9d6970ded 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -46,7 +46,7 @@ tokio = { version = "1", features = [ "signal", ] } tokio-util = "0.7.14" -toml = "0.9.2" +toml = "0.9.4" tracing = { version = "0.1.41", features = ["log"] } tree-sitter = "0.25.8" tree-sitter-bash = "0.25.0" From 6db597ec0c6833018973bbcbe97139d100913304 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 14:25:00 -0700 Subject: [PATCH 0004/1309] chore(deps-dev): bump typescript from 5.8.3 to 5.9.2 in /.github/actions/codex (#1814) [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=typescript&package-manager=bun&previous-version=5.8.3&new-version=5.9.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/codex/bun.lock | 4 ++-- .github/actions/codex/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/codex/bun.lock b/.github/actions/codex/bun.lock index 8b546a5ac6..82e12cc4b6 100644 --- a/.github/actions/codex/bun.lock +++ b/.github/actions/codex/bun.lock @@ -11,7 +11,7 @@ "@types/bun": "^1.2.19", "@types/node": "^24.1.0", "prettier": "^3.6.2", - "typescript": "^5.8.3", + "typescript": "^5.9.2", }, }, }, @@ -68,7 +68,7 @@ "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], "undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], diff --git a/.github/actions/codex/package.json b/.github/actions/codex/package.json index 21817b8a59..6c7ae9002b 100644 --- a/.github/actions/codex/package.json +++ b/.github/actions/codex/package.json @@ -16,6 +16,6 @@ "@types/bun": "^1.2.19", "@types/node": "^24.1.0", "prettier": "^3.6.2", - "typescript": "^5.8.3" + "typescript": "^5.9.2" } } From 89ab5c3f74f6efcf5ac3b5ddb7390a90aecc9df3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 14:26:14 -0700 Subject: [PATCH 0005/1309] chore(deps): bump serde_json from 1.0.141 to 1.0.142 in /codex-rs (#1817) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.141 to 1.0.142.
Release notes

Sourced from serde_json's releases.

v1.0.142

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=serde_json&package-manager=cargo&previous-version=1.0.141&new-version=1.0.142)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- codex-rs/Cargo.lock | 4 ++-- codex-rs/execpolicy/Cargo.toml | 2 +- codex-rs/file-search/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 9d8a027c53..0ad32c3cd4 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3997,9 +3997,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.141" +version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" dependencies = [ "indexmap 2.10.0", "itoa", diff --git a/codex-rs/execpolicy/Cargo.toml b/codex-rs/execpolicy/Cargo.toml index ad003e66c4..9693d5c41f 100644 --- a/codex-rs/execpolicy/Cargo.toml +++ b/codex-rs/execpolicy/Cargo.toml @@ -26,7 +26,7 @@ multimap = "0.10.0" path-absolutize = "3.1.1" regex-lite = "0.1" serde = { version = "1.0.194", features = ["derive"] } -serde_json = "1.0.110" +serde_json = "1.0.142" serde_with = { version = "3", features = ["macros"] } [dev-dependencies] diff --git a/codex-rs/file-search/Cargo.toml b/codex-rs/file-search/Cargo.toml index 3f70377183..bf1e8e687f 100644 --- a/codex-rs/file-search/Cargo.toml +++ b/codex-rs/file-search/Cargo.toml @@ -17,5 +17,5 @@ clap = { version = "4", features = ["derive"] } ignore = "0.4.23" nucleo-matcher = "0.3.1" serde = { version = "1", features = ["derive"] } -serde_json = "1.0.110" +serde_json = "1.0.142" tokio = { version = "1", features = ["full"] } From 7279080edd35fb005d3c02200d1b06165b57f7dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 14:50:53 -0700 Subject: [PATCH 0006/1309] chore(deps): bump tokio from 1.46.1 to 1.47.1 in /codex-rs (#1816) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.46.1 to 1.47.1.
Release notes

Sourced from tokio's releases.

Tokio v1.47.1

1.47.1 (August 1st, 2025)

Fixed

  • process: fix panic from spurious pidfd wakeup (#7494)
  • sync: fix broken link of Python asyncio.Event in SetOnce docs (#7485)

#7485: tokio-rs/tokio#7485 #7494: tokio-rs/tokio#7494

Tokio v1.47.0

1.47.0 (July 25th, 2025)

This release adds poll_proceed and cooperative to the coop module for cooperative scheduling, adds SetOnce to the sync module which provides similar functionality to [std::sync::OnceLock], and adds a new method sync::Notify::notified_owned() which returns an OwnedNotified without a lifetime parameter.

Added

  • coop: add cooperative and poll_proceed (#7405)
  • sync: add SetOnce (#7418)
  • sync: add sync::Notify::notified_owned() (#7465)

Changed

  • deps: upgrade windows-sys 0.52 → 0.59 (#7117)
  • deps: update to socket2 v0.6 (#7443)
  • sync: improve AtomicWaker::wake performance (#7450)

Documented

  • metrics: fix listed feature requirements for some metrics (#7449)
  • runtime: improve safety comments of Readiness<'_> (#7415)

#7405: tokio-rs/tokio#7405 #7415: tokio-rs/tokio#7415 #7418: tokio-rs/tokio#7418 #7449: tokio-rs/tokio#7449 #7450: tokio-rs/tokio#7450 #7465: tokio-rs/tokio#7465

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tokio&package-manager=cargo&previous-version=1.46.1&new-version=1.47.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- codex-rs/Cargo.lock | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 0ad32c3cd4..4daae977b0 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2017,7 +2017,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2", "system-configuration", "tokio", "tower-service", @@ -4188,16 +4188,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.0" @@ -4673,9 +4663,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.46.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", @@ -4686,9 +4676,9 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "slab", - "socket2 0.5.10", + "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] From 3f13ebce10209ab3645f51e7606892b3fd71d47e Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 4 Aug 2025 15:56:32 -0700 Subject: [PATCH 0007/1309] [codex] stop printing error message when --output-last-message is not specified (#1828) Previously, `codex exec` was printing `Warning: no file to write last message to` as a warning to stderr even though `--output-last-message` was not specified, which is wrong. This fixes the code and changes `handle_last_message()` so that it is only called when `last_message_path` is `Some`. --- codex-rs/exec/src/event_processor.rs | 22 +++++++------------ .../src/event_processor_with_human_output.rs | 7 +++--- .../src/event_processor_with_json_output.rs | 7 +++--- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/codex-rs/exec/src/event_processor.rs b/codex-rs/exec/src/event_processor.rs index 741f89d7cb..0f189f3fa2 100644 --- a/codex-rs/exec/src/event_processor.rs +++ b/codex-rs/exec/src/event_processor.rs @@ -44,20 +44,14 @@ pub(crate) fn create_config_summary_entries(config: &Config) -> Vec<(&'static st entries } -pub(crate) fn handle_last_message( - last_agent_message: Option<&str>, - last_message_path: Option<&Path>, -) { - match (last_message_path, last_agent_message) { - (Some(path), Some(msg)) => write_last_message_file(msg, Some(path)), - (Some(path), None) => { - write_last_message_file("", Some(path)); - eprintln!( - "Warning: no last agent message; wrote empty content to {}", - path.display() - ); - } - (None, _) => eprintln!("Warning: no file to write last message to."), +pub(crate) fn handle_last_message(last_agent_message: Option<&str>, output_file: &Path) { + let message = last_agent_message.unwrap_or_default(); + write_last_message_file(message, Some(output_file)); + if last_agent_message.is_none() { + eprintln!( + "Warning: no last agent message; wrote empty content to {}", + output_file.display() + ); } } diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index c290d9336b..7703c138fc 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -170,10 +170,9 @@ impl EventProcessor for EventProcessorWithHumanOutput { // Ignore. } EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message }) => { - handle_last_message( - last_agent_message.as_deref(), - self.last_message_path.as_deref(), - ); + if let Some(output_file) = self.last_message_path.as_deref() { + handle_last_message(last_agent_message.as_deref(), output_file); + } return CodexStatus::InitiateShutdown; } EventMsg::TokenCount(TokenUsage { total_tokens, .. }) => { diff --git a/codex-rs/exec/src/event_processor_with_json_output.rs b/codex-rs/exec/src/event_processor_with_json_output.rs index e7a658b76f..1d153add6e 100644 --- a/codex-rs/exec/src/event_processor_with_json_output.rs +++ b/codex-rs/exec/src/event_processor_with_json_output.rs @@ -46,10 +46,9 @@ impl EventProcessor for EventProcessorWithJsonOutput { CodexStatus::Running } EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message }) => { - handle_last_message( - last_agent_message.as_deref(), - self.last_message_path.as_deref(), - ); + if let Some(output_file) = self.last_message_path.as_deref() { + handle_last_message(last_agent_message.as_deref(), output_file); + } CodexStatus::InitiateShutdown } EventMsg::ShutdownComplete => CodexStatus::Shutdown, From bd171e5206465593a616cd65344a77d17477d51f Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Mon, 4 Aug 2025 16:49:42 -0700 Subject: [PATCH 0008/1309] add raw reasoning --- codex-rs/core/src/chat_completions.rs | 163 ++++++++++++++---- codex-rs/core/src/client.rs | 10 +- codex-rs/core/src/codex.rs | 60 +++++-- codex-rs/core/src/config.rs | 27 ++- codex-rs/core/src/models.rs | 8 + codex-rs/core/src/protocol.rs | 8 + .../src/event_processor_with_human_output.rs | 9 + codex-rs/mcp-server/src/codex_tool_runner.rs | 3 +- codex-rs/mcp-server/src/conversation_loop.rs | 3 +- 9 files changed, 236 insertions(+), 55 deletions(-) diff --git a/codex-rs/core/src/chat_completions.rs b/codex-rs/core/src/chat_completions.rs index 5ede774b1c..d1b8338987 100644 --- a/codex-rs/core/src/chat_completions.rs +++ b/codex-rs/core/src/chat_completions.rs @@ -207,6 +207,7 @@ async fn process_chat_sse( } let mut fn_call_state = FunctionCallState::default(); + let mut assistant_text = String::new(); loop { let sse = match timeout(idle_timeout, stream.next()).await { @@ -254,21 +255,42 @@ async fn process_chat_sse( let choice_opt = chunk.get("choices").and_then(|c| c.get(0)); if let Some(choice) = choice_opt { - // Handle assistant content tokens. + // Handle assistant content tokens as streaming deltas. if let Some(content) = choice .get("delta") .and_then(|d| d.get("content")) .and_then(|c| c.as_str()) { - let item = ResponseItem::Message { - role: "assistant".to_string(), - content: vec![ContentItem::OutputText { - text: content.to_string(), - }], - id: None, - }; + if !content.is_empty() { + assistant_text.push_str(content); + let _ = tx_event + .send(Ok(ResponseEvent::OutputTextDelta(content.to_string()))) + .await; + } + } - let _ = tx_event.send(Ok(ResponseEvent::OutputItemDone(item))).await; + // Forward any reasoning/thinking deltas if present. + if let Some(reasoning) = choice + .get("delta") + .and_then(|d| d.get("reasoning")) + .and_then(|c| c.as_str()) + { + let _ = tx_event + .send(Ok(ResponseEvent::ReasoningSummaryDelta( + reasoning.to_string(), + ))) + .await; + } + if let Some(reasoning_content) = choice + .get("delta") + .and_then(|d| d.get("reasoning_content")) + .and_then(|c| c.as_str()) + { + let _ = tx_event + .send(Ok(ResponseEvent::ReasoningSummaryDelta( + reasoning_content.to_string(), + ))) + .await; } // Handle streaming function / tool calls. @@ -317,7 +339,18 @@ async fn process_chat_sse( let _ = tx_event.send(Ok(ResponseEvent::OutputItemDone(item))).await; } "stop" => { - // Regular turn without tool-call. + // Regular turn without tool-call. Emit the final assistant message + // as a single OutputItemDone so non-delta consumers see the result. + if !assistant_text.is_empty() { + let item = ResponseItem::Message { + role: "assistant".to_string(), + content: vec![ContentItem::OutputText { + text: std::mem::take(&mut assistant_text), + }], + id: None, + }; + let _ = tx_event.send(Ok(ResponseEvent::OutputItemDone(item))).await; + } } _ => {} } @@ -358,7 +391,10 @@ async fn process_chat_sse( pub(crate) struct AggregatedChatStream { inner: S, cumulative: String, - pending_completed: Option, + cumulative_reasoning: String, + pending: std::collections::VecDeque, + // When true, do not emit a cumulative assistant message at Completed. + streaming_mode: bool, } impl Stream for AggregatedChatStream @@ -370,8 +406,8 @@ where fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.get_mut(); - // First, flush any buffered Completed event from the previous call. - if let Some(ev) = this.pending_completed.take() { + // First, flush any buffered events from the previous call. + if let Some(ev) = this.pending.pop_front() { return Poll::Ready(Some(Ok(ev))); } @@ -388,16 +424,21 @@ where let is_assistant_delta = matches!(&item, crate::models::ResponseItem::Message { role, .. } if role == "assistant"); if is_assistant_delta { - if let crate::models::ResponseItem::Message { content, .. } = &item { - if let Some(text) = content.iter().find_map(|c| match c { - crate::models::ContentItem::OutputText { text } => Some(text), - _ => None, - }) { - this.cumulative.push_str(text); + // Only use the final assistant message if we have not + // seen any deltas; otherwise, deltas already built the + // cumulative text and this would duplicate it. + if this.cumulative.is_empty() { + if let crate::models::ResponseItem::Message { content, .. } = &item { + if let Some(text) = content.iter().find_map(|c| match c { + crate::models::ContentItem::OutputText { text } => Some(text), + _ => None, + }) { + this.cumulative.push_str(text); + } } } - // Swallow partial assistant chunk; keep polling. + // Swallow assistant message here; emit on Completed. continue; } @@ -408,24 +449,48 @@ where response_id, token_usage, }))) => { + // Build any aggregated items in the correct order: Reasoning first, then Message. + let mut emitted_any = false; + + if !this.cumulative_reasoning.is_empty() { + let aggregated_reasoning = crate::models::ResponseItem::Reasoning { + id: String::new(), + summary: vec![ + crate::models::ReasoningItemReasoningSummary::SummaryText { + text: std::mem::take(&mut this.cumulative_reasoning), + }, + ], + content: None, + encrypted_content: None, + }; + this.pending + .push_back(ResponseEvent::OutputItemDone(aggregated_reasoning)); + emitted_any = true; + } + if !this.cumulative.is_empty() { - let aggregated_item = crate::models::ResponseItem::Message { + let aggregated_message = crate::models::ResponseItem::Message { id: None, role: "assistant".to_string(), content: vec![crate::models::ContentItem::OutputText { text: std::mem::take(&mut this.cumulative), }], }; + this.pending + .push_back(ResponseEvent::OutputItemDone(aggregated_message)); + emitted_any = true; + } - // Buffer Completed so it is returned *after* the aggregated message. - this.pending_completed = Some(ResponseEvent::Completed { - response_id, - token_usage, + // Always emit Completed last when anything was aggregated. + if emitted_any { + this.pending.push_back(ResponseEvent::Completed { + response_id: response_id.clone(), + token_usage: token_usage.clone(), }); - - return Poll::Ready(Some(Ok(ResponseEvent::OutputItemDone( - aggregated_item, - )))); + // Return the first pending event now. + if let Some(ev) = this.pending.pop_front() { + return Poll::Ready(Some(Ok(ev))); + } } // Nothing aggregated – forward Completed directly. @@ -439,11 +504,25 @@ where // will never appear in a Chat Completions stream. continue; } - Poll::Ready(Some(Ok(ResponseEvent::OutputTextDelta(_)))) - | Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryDelta(_)))) => { - // Deltas are ignored here since aggregation waits for the - // final OutputItemDone. - continue; + Poll::Ready(Some(Ok(ResponseEvent::OutputTextDelta(delta)))) => { + // Always accumulate deltas so we can emit a final OutputItemDone at Completed. + this.cumulative.push_str(&delta); + if this.streaming_mode { + // In streaming mode, also forward the delta immediately. + return Poll::Ready(Some(Ok(ResponseEvent::OutputTextDelta(delta)))); + } else { + continue; + } + } + Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryDelta(delta)))) => { + // Always accumulate reasoning deltas so we can emit a final Reasoning item at Completed. + this.cumulative_reasoning.push_str(&delta); + if this.streaming_mode { + // In streaming mode, also forward the delta immediately. + return Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryDelta(delta)))); + } else { + continue; + } } } } @@ -475,9 +554,23 @@ pub(crate) trait AggregateStreamExt: Stream> + Size AggregatedChatStream { inner: self, cumulative: String::new(), - pending_completed: None, + cumulative_reasoning: String::new(), + pending: std::collections::VecDeque::new(), + streaming_mode: false, } } } impl AggregateStreamExt for T where T: Stream> + Sized {} + +impl AggregatedChatStream { + pub(crate) fn streaming_mode(inner: S) -> Self { + AggregatedChatStream { + inner, + cumulative: String::new(), + cumulative_reasoning: String::new(), + pending: std::collections::VecDeque::new(), + streaming_mode: true, + } + } +} diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index b9ea6b13f4..8685bc54d3 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -93,7 +93,13 @@ impl ModelClient { // Wrap it with the aggregation adapter so callers see *only* // the final assistant message per turn (matching the // behaviour of the Responses API). - let mut aggregated = response_stream.aggregate(); + let mut aggregated = if self.config.show_reasoning_content + && !self.config.hide_agent_reasoning + { + crate::chat_completions::AggregatedChatStream::streaming_mode(response_stream) + } else { + response_stream.aggregate() + }; // Bridge the aggregated stream back into a standard // `ResponseStream` by forwarding events through a channel. @@ -438,7 +444,7 @@ async fn process_sse( } } } - "response.reasoning_summary_text.delta" => { + "response.reasoning_summary_text.delta" | "response.reasoning_text.delta" => { if let Some(delta) = event.delta { let event = ResponseEvent::ReasoningSummaryDelta(delta); if tx_event.send(Ok(event)).await.is_err() { diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 568d87c4a8..18bcf6261d 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -56,6 +56,7 @@ use crate::mcp_tool_call::handle_mcp_tool_call; use crate::models::ContentItem; use crate::models::FunctionCallOutputPayload; use crate::models::LocalShellAction; +use crate::models::ReasoningItemContent; use crate::models::ReasoningItemReasoningSummary; use crate::models::ResponseInputItem; use crate::models::ResponseItem; @@ -64,6 +65,7 @@ use crate::plan_tool::handle_update_plan; use crate::project_doc::get_user_instructions; use crate::protocol::AgentMessageDeltaEvent; use crate::protocol::AgentMessageEvent; +use crate::protocol::AgentReasoningContentEvent; use crate::protocol::AgentReasoningDeltaEvent; use crate::protocol::AgentReasoningEvent; use crate::protocol::ApplyPatchApprovalRequestEvent; @@ -227,6 +229,8 @@ pub(crate) struct Session { state: Mutex, codex_linux_sandbox_exe: Option, user_shell: shell::Shell, + show_reasoning_content: bool, + hide_agent_reasoning: bool, } impl Session { @@ -822,6 +826,8 @@ async fn submission_loop( codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(), disable_response_storage, user_shell: default_shell, + show_reasoning_content: config.show_reasoning_content, + hide_agent_reasoning: config.hide_agent_reasoning, })); // Patch restored state into the newly created session. @@ -1132,6 +1138,7 @@ async fn run_task(sess: Arc, sub_id: String, input: Vec) { ResponseItem::Reasoning { id, summary, + content, encrypted_content, }, None, @@ -1139,6 +1146,7 @@ async fn run_task(sess: Arc, sub_id: String, input: Vec) { items_to_record_in_conversation_history.push(ResponseItem::Reasoning { id: id.clone(), summary: summary.clone(), + content: content.clone(), encrypted_content: encrypted_content.clone(), }); } @@ -1381,11 +1389,13 @@ async fn try_run_turn( sess.tx_event.send(event).await.ok(); } ResponseEvent::ReasoningSummaryDelta(delta) => { - let event = Event { - id: sub_id.to_string(), - msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta }), - }; - sess.tx_event.send(event).await.ok(); + if !sess.hide_agent_reasoning { + let event = Event { + id: sub_id.to_string(), + msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta }), + }; + sess.tx_event.send(event).await.ok(); + } } } } @@ -1493,16 +1503,36 @@ async fn handle_response_item( } None } - ResponseItem::Reasoning { summary, .. } => { - for item in summary { - let text = match item { - ReasoningItemReasoningSummary::SummaryText { text } => text, - }; - let event = Event { - id: sub_id.to_string(), - msg: EventMsg::AgentReasoning(AgentReasoningEvent { text }), - }; - sess.tx_event.send(event).await.ok(); + ResponseItem::Reasoning { + id: _, + summary, + content, + encrypted_content: _, + } => { + if !sess.hide_agent_reasoning { + for item in summary { + let text = match item { + ReasoningItemReasoningSummary::SummaryText { text } => text, + }; + let event = Event { + id: sub_id.to_string(), + msg: EventMsg::AgentReasoning(AgentReasoningEvent { text }), + }; + sess.tx_event.send(event).await.ok(); + } + } + if !sess.hide_agent_reasoning && sess.show_reasoning_content && content.is_some() { + let content = content.unwrap(); + for item in content { + let text = match item { + ReasoningItemContent::ReasoningText { text } => text, + }; + let event = Event { + id: sub_id.to_string(), + msg: EventMsg::AgentReasoningContent(AgentReasoningContentEvent { text }), + }; + sess.tx_event.send(event).await.ok(); + } } None } diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index b43dc56ba0..3277ca08e9 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -57,6 +57,10 @@ pub struct Config { /// users are only interested in the final agent responses. pub hide_agent_reasoning: bool, + /// When `true`, the raw chain-of-thought text from reasoning events will be + /// displayed in the UI in addition to the reasoning summaries. + pub show_reasoning_content: bool, + /// Disable server-side response storage (sends the full conversation /// context with every request). Currently necessary for OpenAI customers /// who have opted into Zero Data Retention (ZDR). @@ -325,6 +329,10 @@ pub struct ConfigToml { /// UI/output. Defaults to `false`. pub hide_agent_reasoning: Option, + /// When set to `true`, raw chain-of-thought text from reasoning events will + /// be shown in the UI. + pub show_reasoning_content: Option, + pub model_reasoning_effort: Option, pub model_reasoning_summary: Option, @@ -488,6 +496,19 @@ impl Config { Self::get_base_instructions(experimental_instructions_path, &resolved_cwd)?; let base_instructions = base_instructions.or(file_base_instructions); + // Resolve hide/show reasoning flags with consistent precedence: + // if hide is true, force show_reasoning_content to false. + let hide_agent_reasoning_val = cfg.hide_agent_reasoning.unwrap_or(false); + let show_reasoning_content_val = if hide_agent_reasoning_val { + false + } else { + cfg.show_reasoning_content.unwrap_or(false) + }; + + if cfg.hide_agent_reasoning == Some(true) && cfg.show_reasoning_content == Some(true) { + tracing::warn!("Ignoring show_reasoning_content because hide_agent_reasoning is true"); + } + let config = Self { model, model_context_window, @@ -517,7 +538,8 @@ impl Config { tui: cfg.tui.unwrap_or_default(), codex_linux_sandbox_exe, - hide_agent_reasoning: cfg.hide_agent_reasoning.unwrap_or(false), + hide_agent_reasoning: hide_agent_reasoning_val, + show_reasoning_content: show_reasoning_content_val, model_reasoning_effort: config_profile .model_reasoning_effort .or(cfg.model_reasoning_effort) @@ -891,6 +913,7 @@ disable_response_storage = true tui: Tui::default(), codex_linux_sandbox_exe: None, hide_agent_reasoning: false, + show_reasoning_content: false, model_reasoning_effort: ReasoningEffort::High, model_reasoning_summary: ReasoningSummary::Detailed, model_supports_reasoning_summaries: false, @@ -941,6 +964,7 @@ disable_response_storage = true tui: Tui::default(), codex_linux_sandbox_exe: None, hide_agent_reasoning: false, + show_reasoning_content: false, model_reasoning_effort: ReasoningEffort::default(), model_reasoning_summary: ReasoningSummary::default(), model_supports_reasoning_summaries: false, @@ -1006,6 +1030,7 @@ disable_response_storage = true tui: Tui::default(), codex_linux_sandbox_exe: None, hide_agent_reasoning: false, + show_reasoning_content: false, model_reasoning_effort: ReasoningEffort::default(), model_reasoning_summary: ReasoningSummary::default(), model_supports_reasoning_summaries: false, diff --git a/codex-rs/core/src/models.rs b/codex-rs/core/src/models.rs index 166404915a..98d8727e77 100644 --- a/codex-rs/core/src/models.rs +++ b/codex-rs/core/src/models.rs @@ -45,6 +45,8 @@ pub enum ResponseItem { Reasoning { id: String, summary: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + content: Option>, encrypted_content: Option, }, LocalShellCall { @@ -136,6 +138,12 @@ pub enum ReasoningItemReasoningSummary { SummaryText { text: String }, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ReasoningItemContent { + ReasoningText { text: String }, +} + impl From> for ResponseInputItem { fn from(items: Vec) -> Self { Self::Message { diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs index 82591a2c78..1e0733628c 100644 --- a/codex-rs/core/src/protocol.rs +++ b/codex-rs/core/src/protocol.rs @@ -359,6 +359,9 @@ pub enum EventMsg { /// Agent reasoning delta event from agent. AgentReasoningDelta(AgentReasoningDeltaEvent), + /// Raw chain-of-thought from agent. + AgentReasoningContent(AgentReasoningContentEvent), + /// Ack the client's configure message. SessionConfigured(SessionConfiguredEvent), @@ -464,6 +467,11 @@ pub struct AgentReasoningEvent { pub text: String, } +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct AgentReasoningContentEvent { + pub text: String, +} + #[derive(Debug, Clone, Deserialize, Serialize)] pub struct AgentReasoningDeltaEvent { pub delta: String, diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 7703c138fc..1f8fe3c031 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -4,6 +4,7 @@ use codex_core::config::Config; use codex_core::plan_tool::UpdatePlanArgs; use codex_core::protocol::AgentMessageDeltaEvent; use codex_core::protocol::AgentMessageEvent; +use codex_core::protocol::AgentReasoningContentEvent; use codex_core::protocol::AgentReasoningDeltaEvent; use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::ErrorEvent; @@ -203,6 +204,14 @@ impl EventProcessor for EventProcessorWithHumanOutput { #[allow(clippy::expect_used)] std::io::stdout().flush().expect("could not flush stdout"); } + EventMsg::AgentReasoningContent(AgentReasoningContentEvent { text }) => { + if !self.show_agent_reasoning { + return CodexStatus::Running; + } + print!("{text}"); + #[allow(clippy::expect_used)] + std::io::stdout().flush().expect("could not flush stdout"); + } EventMsg::AgentMessage(AgentMessageEvent { message }) => { // if answer_started is false, this means we haven't received any // delta. Thus, we need to print the message as a new answer. diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index 205dfa4631..3d32d8de52 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -252,7 +252,8 @@ async fn run_codex_tool_session_inner( EventMsg::AgentMessage(AgentMessageEvent { .. }) => { // TODO: think how we want to support this in the MCP } - EventMsg::TaskStarted + EventMsg::AgentReasoningContent(_) + | EventMsg::TaskStarted | EventMsg::TokenCount(_) | EventMsg::AgentReasoning(_) | EventMsg::McpToolCallBegin(_) diff --git a/codex-rs/mcp-server/src/conversation_loop.rs b/codex-rs/mcp-server/src/conversation_loop.rs index 1db39a2306..5b95c313f4 100644 --- a/codex-rs/mcp-server/src/conversation_loop.rs +++ b/codex-rs/mcp-server/src/conversation_loop.rs @@ -90,7 +90,8 @@ pub async fn run_conversation_loop( EventMsg::AgentMessage(AgentMessageEvent { .. }) => { // TODO: think how we want to support this in the MCP } - EventMsg::TaskStarted + EventMsg::AgentReasoningContent(_) + | EventMsg::TaskStarted | EventMsg::TokenCount(_) | EventMsg::AgentReasoning(_) | EventMsg::McpToolCallBegin(_) From 1a33de34b06eea9731ae27d2a2ab532af586e782 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Mon, 4 Aug 2025 16:56:52 -0700 Subject: [PATCH 0009/1309] unify flag --- codex-rs/core/src/client.rs | 4 +--- codex-rs/core/src/codex.rs | 4 +--- codex-rs/core/src/config.rs | 21 --------------------- 3 files changed, 2 insertions(+), 27 deletions(-) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 8685bc54d3..fd530e0c6d 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -93,9 +93,7 @@ impl ModelClient { // Wrap it with the aggregation adapter so callers see *only* // the final assistant message per turn (matching the // behaviour of the Responses API). - let mut aggregated = if self.config.show_reasoning_content - && !self.config.hide_agent_reasoning - { + let mut aggregated = if !self.config.hide_agent_reasoning { crate::chat_completions::AggregatedChatStream::streaming_mode(response_stream) } else { response_stream.aggregate() diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 18bcf6261d..caebaed233 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -229,7 +229,6 @@ pub(crate) struct Session { state: Mutex, codex_linux_sandbox_exe: Option, user_shell: shell::Shell, - show_reasoning_content: bool, hide_agent_reasoning: bool, } @@ -826,7 +825,6 @@ async fn submission_loop( codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(), disable_response_storage, user_shell: default_shell, - show_reasoning_content: config.show_reasoning_content, hide_agent_reasoning: config.hide_agent_reasoning, })); @@ -1521,7 +1519,7 @@ async fn handle_response_item( sess.tx_event.send(event).await.ok(); } } - if !sess.hide_agent_reasoning && sess.show_reasoning_content && content.is_some() { + if !sess.hide_agent_reasoning && content.is_some() { let content = content.unwrap(); for item in content { let text = match item { diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 3277ca08e9..302c468b66 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -57,10 +57,6 @@ pub struct Config { /// users are only interested in the final agent responses. pub hide_agent_reasoning: bool, - /// When `true`, the raw chain-of-thought text from reasoning events will be - /// displayed in the UI in addition to the reasoning summaries. - pub show_reasoning_content: bool, - /// Disable server-side response storage (sends the full conversation /// context with every request). Currently necessary for OpenAI customers /// who have opted into Zero Data Retention (ZDR). @@ -329,10 +325,6 @@ pub struct ConfigToml { /// UI/output. Defaults to `false`. pub hide_agent_reasoning: Option, - /// When set to `true`, raw chain-of-thought text from reasoning events will - /// be shown in the UI. - pub show_reasoning_content: Option, - pub model_reasoning_effort: Option, pub model_reasoning_summary: Option, @@ -499,15 +491,6 @@ impl Config { // Resolve hide/show reasoning flags with consistent precedence: // if hide is true, force show_reasoning_content to false. let hide_agent_reasoning_val = cfg.hide_agent_reasoning.unwrap_or(false); - let show_reasoning_content_val = if hide_agent_reasoning_val { - false - } else { - cfg.show_reasoning_content.unwrap_or(false) - }; - - if cfg.hide_agent_reasoning == Some(true) && cfg.show_reasoning_content == Some(true) { - tracing::warn!("Ignoring show_reasoning_content because hide_agent_reasoning is true"); - } let config = Self { model, @@ -539,7 +522,6 @@ impl Config { codex_linux_sandbox_exe, hide_agent_reasoning: hide_agent_reasoning_val, - show_reasoning_content: show_reasoning_content_val, model_reasoning_effort: config_profile .model_reasoning_effort .or(cfg.model_reasoning_effort) @@ -913,7 +895,6 @@ disable_response_storage = true tui: Tui::default(), codex_linux_sandbox_exe: None, hide_agent_reasoning: false, - show_reasoning_content: false, model_reasoning_effort: ReasoningEffort::High, model_reasoning_summary: ReasoningSummary::Detailed, model_supports_reasoning_summaries: false, @@ -964,7 +945,6 @@ disable_response_storage = true tui: Tui::default(), codex_linux_sandbox_exe: None, hide_agent_reasoning: false, - show_reasoning_content: false, model_reasoning_effort: ReasoningEffort::default(), model_reasoning_summary: ReasoningSummary::default(), model_supports_reasoning_summaries: false, @@ -1030,7 +1010,6 @@ disable_response_storage = true tui: Tui::default(), codex_linux_sandbox_exe: None, hide_agent_reasoning: false, - show_reasoning_content: false, model_reasoning_effort: ReasoningEffort::default(), model_reasoning_summary: ReasoningSummary::default(), model_supports_reasoning_summaries: false, From e38ce39c514d035d0638c82c85d8fe1c4e120fc1 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Mon, 4 Aug 2025 17:03:24 -0700 Subject: [PATCH 0010/1309] Revert to 3f13ebce10209ab3645f51e7606892b3fd71d47e without rewriting history. Wrong merge --- codex-rs/core/src/chat_completions.rs | 163 ++++-------------- codex-rs/core/src/client.rs | 8 +- codex-rs/core/src/codex.rs | 58 ++----- codex-rs/core/src/config.rs | 6 +- codex-rs/core/src/models.rs | 8 - codex-rs/core/src/protocol.rs | 8 - .../src/event_processor_with_human_output.rs | 9 - codex-rs/mcp-server/src/codex_tool_runner.rs | 3 +- codex-rs/mcp-server/src/conversation_loop.rs | 3 +- 9 files changed, 55 insertions(+), 211 deletions(-) diff --git a/codex-rs/core/src/chat_completions.rs b/codex-rs/core/src/chat_completions.rs index d1b8338987..5ede774b1c 100644 --- a/codex-rs/core/src/chat_completions.rs +++ b/codex-rs/core/src/chat_completions.rs @@ -207,7 +207,6 @@ async fn process_chat_sse( } let mut fn_call_state = FunctionCallState::default(); - let mut assistant_text = String::new(); loop { let sse = match timeout(idle_timeout, stream.next()).await { @@ -255,42 +254,21 @@ async fn process_chat_sse( let choice_opt = chunk.get("choices").and_then(|c| c.get(0)); if let Some(choice) = choice_opt { - // Handle assistant content tokens as streaming deltas. + // Handle assistant content tokens. if let Some(content) = choice .get("delta") .and_then(|d| d.get("content")) .and_then(|c| c.as_str()) { - if !content.is_empty() { - assistant_text.push_str(content); - let _ = tx_event - .send(Ok(ResponseEvent::OutputTextDelta(content.to_string()))) - .await; - } - } + let item = ResponseItem::Message { + role: "assistant".to_string(), + content: vec![ContentItem::OutputText { + text: content.to_string(), + }], + id: None, + }; - // Forward any reasoning/thinking deltas if present. - if let Some(reasoning) = choice - .get("delta") - .and_then(|d| d.get("reasoning")) - .and_then(|c| c.as_str()) - { - let _ = tx_event - .send(Ok(ResponseEvent::ReasoningSummaryDelta( - reasoning.to_string(), - ))) - .await; - } - if let Some(reasoning_content) = choice - .get("delta") - .and_then(|d| d.get("reasoning_content")) - .and_then(|c| c.as_str()) - { - let _ = tx_event - .send(Ok(ResponseEvent::ReasoningSummaryDelta( - reasoning_content.to_string(), - ))) - .await; + let _ = tx_event.send(Ok(ResponseEvent::OutputItemDone(item))).await; } // Handle streaming function / tool calls. @@ -339,18 +317,7 @@ async fn process_chat_sse( let _ = tx_event.send(Ok(ResponseEvent::OutputItemDone(item))).await; } "stop" => { - // Regular turn without tool-call. Emit the final assistant message - // as a single OutputItemDone so non-delta consumers see the result. - if !assistant_text.is_empty() { - let item = ResponseItem::Message { - role: "assistant".to_string(), - content: vec![ContentItem::OutputText { - text: std::mem::take(&mut assistant_text), - }], - id: None, - }; - let _ = tx_event.send(Ok(ResponseEvent::OutputItemDone(item))).await; - } + // Regular turn without tool-call. } _ => {} } @@ -391,10 +358,7 @@ async fn process_chat_sse( pub(crate) struct AggregatedChatStream { inner: S, cumulative: String, - cumulative_reasoning: String, - pending: std::collections::VecDeque, - // When true, do not emit a cumulative assistant message at Completed. - streaming_mode: bool, + pending_completed: Option, } impl Stream for AggregatedChatStream @@ -406,8 +370,8 @@ where fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.get_mut(); - // First, flush any buffered events from the previous call. - if let Some(ev) = this.pending.pop_front() { + // First, flush any buffered Completed event from the previous call. + if let Some(ev) = this.pending_completed.take() { return Poll::Ready(Some(Ok(ev))); } @@ -424,21 +388,16 @@ where let is_assistant_delta = matches!(&item, crate::models::ResponseItem::Message { role, .. } if role == "assistant"); if is_assistant_delta { - // Only use the final assistant message if we have not - // seen any deltas; otherwise, deltas already built the - // cumulative text and this would duplicate it. - if this.cumulative.is_empty() { - if let crate::models::ResponseItem::Message { content, .. } = &item { - if let Some(text) = content.iter().find_map(|c| match c { - crate::models::ContentItem::OutputText { text } => Some(text), - _ => None, - }) { - this.cumulative.push_str(text); - } + if let crate::models::ResponseItem::Message { content, .. } = &item { + if let Some(text) = content.iter().find_map(|c| match c { + crate::models::ContentItem::OutputText { text } => Some(text), + _ => None, + }) { + this.cumulative.push_str(text); } } - // Swallow assistant message here; emit on Completed. + // Swallow partial assistant chunk; keep polling. continue; } @@ -449,48 +408,24 @@ where response_id, token_usage, }))) => { - // Build any aggregated items in the correct order: Reasoning first, then Message. - let mut emitted_any = false; - - if !this.cumulative_reasoning.is_empty() { - let aggregated_reasoning = crate::models::ResponseItem::Reasoning { - id: String::new(), - summary: vec![ - crate::models::ReasoningItemReasoningSummary::SummaryText { - text: std::mem::take(&mut this.cumulative_reasoning), - }, - ], - content: None, - encrypted_content: None, - }; - this.pending - .push_back(ResponseEvent::OutputItemDone(aggregated_reasoning)); - emitted_any = true; - } - if !this.cumulative.is_empty() { - let aggregated_message = crate::models::ResponseItem::Message { + let aggregated_item = crate::models::ResponseItem::Message { id: None, role: "assistant".to_string(), content: vec![crate::models::ContentItem::OutputText { text: std::mem::take(&mut this.cumulative), }], }; - this.pending - .push_back(ResponseEvent::OutputItemDone(aggregated_message)); - emitted_any = true; - } - // Always emit Completed last when anything was aggregated. - if emitted_any { - this.pending.push_back(ResponseEvent::Completed { - response_id: response_id.clone(), - token_usage: token_usage.clone(), + // Buffer Completed so it is returned *after* the aggregated message. + this.pending_completed = Some(ResponseEvent::Completed { + response_id, + token_usage, }); - // Return the first pending event now. - if let Some(ev) = this.pending.pop_front() { - return Poll::Ready(Some(Ok(ev))); - } + + return Poll::Ready(Some(Ok(ResponseEvent::OutputItemDone( + aggregated_item, + )))); } // Nothing aggregated – forward Completed directly. @@ -504,25 +439,11 @@ where // will never appear in a Chat Completions stream. continue; } - Poll::Ready(Some(Ok(ResponseEvent::OutputTextDelta(delta)))) => { - // Always accumulate deltas so we can emit a final OutputItemDone at Completed. - this.cumulative.push_str(&delta); - if this.streaming_mode { - // In streaming mode, also forward the delta immediately. - return Poll::Ready(Some(Ok(ResponseEvent::OutputTextDelta(delta)))); - } else { - continue; - } - } - Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryDelta(delta)))) => { - // Always accumulate reasoning deltas so we can emit a final Reasoning item at Completed. - this.cumulative_reasoning.push_str(&delta); - if this.streaming_mode { - // In streaming mode, also forward the delta immediately. - return Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryDelta(delta)))); - } else { - continue; - } + Poll::Ready(Some(Ok(ResponseEvent::OutputTextDelta(_)))) + | Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryDelta(_)))) => { + // Deltas are ignored here since aggregation waits for the + // final OutputItemDone. + continue; } } } @@ -554,23 +475,9 @@ pub(crate) trait AggregateStreamExt: Stream> + Size AggregatedChatStream { inner: self, cumulative: String::new(), - cumulative_reasoning: String::new(), - pending: std::collections::VecDeque::new(), - streaming_mode: false, + pending_completed: None, } } } impl AggregateStreamExt for T where T: Stream> + Sized {} - -impl AggregatedChatStream { - pub(crate) fn streaming_mode(inner: S) -> Self { - AggregatedChatStream { - inner, - cumulative: String::new(), - cumulative_reasoning: String::new(), - pending: std::collections::VecDeque::new(), - streaming_mode: true, - } - } -} diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index fd530e0c6d..b9ea6b13f4 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -93,11 +93,7 @@ impl ModelClient { // Wrap it with the aggregation adapter so callers see *only* // the final assistant message per turn (matching the // behaviour of the Responses API). - let mut aggregated = if !self.config.hide_agent_reasoning { - crate::chat_completions::AggregatedChatStream::streaming_mode(response_stream) - } else { - response_stream.aggregate() - }; + let mut aggregated = response_stream.aggregate(); // Bridge the aggregated stream back into a standard // `ResponseStream` by forwarding events through a channel. @@ -442,7 +438,7 @@ async fn process_sse( } } } - "response.reasoning_summary_text.delta" | "response.reasoning_text.delta" => { + "response.reasoning_summary_text.delta" => { if let Some(delta) = event.delta { let event = ResponseEvent::ReasoningSummaryDelta(delta); if tx_event.send(Ok(event)).await.is_err() { diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index caebaed233..568d87c4a8 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -56,7 +56,6 @@ use crate::mcp_tool_call::handle_mcp_tool_call; use crate::models::ContentItem; use crate::models::FunctionCallOutputPayload; use crate::models::LocalShellAction; -use crate::models::ReasoningItemContent; use crate::models::ReasoningItemReasoningSummary; use crate::models::ResponseInputItem; use crate::models::ResponseItem; @@ -65,7 +64,6 @@ use crate::plan_tool::handle_update_plan; use crate::project_doc::get_user_instructions; use crate::protocol::AgentMessageDeltaEvent; use crate::protocol::AgentMessageEvent; -use crate::protocol::AgentReasoningContentEvent; use crate::protocol::AgentReasoningDeltaEvent; use crate::protocol::AgentReasoningEvent; use crate::protocol::ApplyPatchApprovalRequestEvent; @@ -229,7 +227,6 @@ pub(crate) struct Session { state: Mutex, codex_linux_sandbox_exe: Option, user_shell: shell::Shell, - hide_agent_reasoning: bool, } impl Session { @@ -825,7 +822,6 @@ async fn submission_loop( codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(), disable_response_storage, user_shell: default_shell, - hide_agent_reasoning: config.hide_agent_reasoning, })); // Patch restored state into the newly created session. @@ -1136,7 +1132,6 @@ async fn run_task(sess: Arc, sub_id: String, input: Vec) { ResponseItem::Reasoning { id, summary, - content, encrypted_content, }, None, @@ -1144,7 +1139,6 @@ async fn run_task(sess: Arc, sub_id: String, input: Vec) { items_to_record_in_conversation_history.push(ResponseItem::Reasoning { id: id.clone(), summary: summary.clone(), - content: content.clone(), encrypted_content: encrypted_content.clone(), }); } @@ -1387,13 +1381,11 @@ async fn try_run_turn( sess.tx_event.send(event).await.ok(); } ResponseEvent::ReasoningSummaryDelta(delta) => { - if !sess.hide_agent_reasoning { - let event = Event { - id: sub_id.to_string(), - msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta }), - }; - sess.tx_event.send(event).await.ok(); - } + let event = Event { + id: sub_id.to_string(), + msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta }), + }; + sess.tx_event.send(event).await.ok(); } } } @@ -1501,36 +1493,16 @@ async fn handle_response_item( } None } - ResponseItem::Reasoning { - id: _, - summary, - content, - encrypted_content: _, - } => { - if !sess.hide_agent_reasoning { - for item in summary { - let text = match item { - ReasoningItemReasoningSummary::SummaryText { text } => text, - }; - let event = Event { - id: sub_id.to_string(), - msg: EventMsg::AgentReasoning(AgentReasoningEvent { text }), - }; - sess.tx_event.send(event).await.ok(); - } - } - if !sess.hide_agent_reasoning && content.is_some() { - let content = content.unwrap(); - for item in content { - let text = match item { - ReasoningItemContent::ReasoningText { text } => text, - }; - let event = Event { - id: sub_id.to_string(), - msg: EventMsg::AgentReasoningContent(AgentReasoningContentEvent { text }), - }; - sess.tx_event.send(event).await.ok(); - } + ResponseItem::Reasoning { summary, .. } => { + for item in summary { + let text = match item { + ReasoningItemReasoningSummary::SummaryText { text } => text, + }; + let event = Event { + id: sub_id.to_string(), + msg: EventMsg::AgentReasoning(AgentReasoningEvent { text }), + }; + sess.tx_event.send(event).await.ok(); } None } diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 302c468b66..b43dc56ba0 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -488,10 +488,6 @@ impl Config { Self::get_base_instructions(experimental_instructions_path, &resolved_cwd)?; let base_instructions = base_instructions.or(file_base_instructions); - // Resolve hide/show reasoning flags with consistent precedence: - // if hide is true, force show_reasoning_content to false. - let hide_agent_reasoning_val = cfg.hide_agent_reasoning.unwrap_or(false); - let config = Self { model, model_context_window, @@ -521,7 +517,7 @@ impl Config { tui: cfg.tui.unwrap_or_default(), codex_linux_sandbox_exe, - hide_agent_reasoning: hide_agent_reasoning_val, + hide_agent_reasoning: cfg.hide_agent_reasoning.unwrap_or(false), model_reasoning_effort: config_profile .model_reasoning_effort .or(cfg.model_reasoning_effort) diff --git a/codex-rs/core/src/models.rs b/codex-rs/core/src/models.rs index 98d8727e77..166404915a 100644 --- a/codex-rs/core/src/models.rs +++ b/codex-rs/core/src/models.rs @@ -45,8 +45,6 @@ pub enum ResponseItem { Reasoning { id: String, summary: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - content: Option>, encrypted_content: Option, }, LocalShellCall { @@ -138,12 +136,6 @@ pub enum ReasoningItemReasoningSummary { SummaryText { text: String }, } -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum ReasoningItemContent { - ReasoningText { text: String }, -} - impl From> for ResponseInputItem { fn from(items: Vec) -> Self { Self::Message { diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs index 1e0733628c..82591a2c78 100644 --- a/codex-rs/core/src/protocol.rs +++ b/codex-rs/core/src/protocol.rs @@ -359,9 +359,6 @@ pub enum EventMsg { /// Agent reasoning delta event from agent. AgentReasoningDelta(AgentReasoningDeltaEvent), - /// Raw chain-of-thought from agent. - AgentReasoningContent(AgentReasoningContentEvent), - /// Ack the client's configure message. SessionConfigured(SessionConfiguredEvent), @@ -467,11 +464,6 @@ pub struct AgentReasoningEvent { pub text: String, } -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct AgentReasoningContentEvent { - pub text: String, -} - #[derive(Debug, Clone, Deserialize, Serialize)] pub struct AgentReasoningDeltaEvent { pub delta: String, diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 1f8fe3c031..7703c138fc 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -4,7 +4,6 @@ use codex_core::config::Config; use codex_core::plan_tool::UpdatePlanArgs; use codex_core::protocol::AgentMessageDeltaEvent; use codex_core::protocol::AgentMessageEvent; -use codex_core::protocol::AgentReasoningContentEvent; use codex_core::protocol::AgentReasoningDeltaEvent; use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::ErrorEvent; @@ -204,14 +203,6 @@ impl EventProcessor for EventProcessorWithHumanOutput { #[allow(clippy::expect_used)] std::io::stdout().flush().expect("could not flush stdout"); } - EventMsg::AgentReasoningContent(AgentReasoningContentEvent { text }) => { - if !self.show_agent_reasoning { - return CodexStatus::Running; - } - print!("{text}"); - #[allow(clippy::expect_used)] - std::io::stdout().flush().expect("could not flush stdout"); - } EventMsg::AgentMessage(AgentMessageEvent { message }) => { // if answer_started is false, this means we haven't received any // delta. Thus, we need to print the message as a new answer. diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index 3d32d8de52..205dfa4631 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -252,8 +252,7 @@ async fn run_codex_tool_session_inner( EventMsg::AgentMessage(AgentMessageEvent { .. }) => { // TODO: think how we want to support this in the MCP } - EventMsg::AgentReasoningContent(_) - | EventMsg::TaskStarted + EventMsg::TaskStarted | EventMsg::TokenCount(_) | EventMsg::AgentReasoning(_) | EventMsg::McpToolCallBegin(_) diff --git a/codex-rs/mcp-server/src/conversation_loop.rs b/codex-rs/mcp-server/src/conversation_loop.rs index 5b95c313f4..1db39a2306 100644 --- a/codex-rs/mcp-server/src/conversation_loop.rs +++ b/codex-rs/mcp-server/src/conversation_loop.rs @@ -90,8 +90,7 @@ pub async fn run_conversation_loop( EventMsg::AgentMessage(AgentMessageEvent { .. }) => { // TODO: think how we want to support this in the MCP } - EventMsg::AgentReasoningContent(_) - | EventMsg::TaskStarted + EventMsg::TaskStarted | EventMsg::TokenCount(_) | EventMsg::AgentReasoning(_) | EventMsg::McpToolCallBegin(_) From 84bcadb8d92d6a0b694cb19c865d6aafac14d6f3 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Mon, 4 Aug 2025 18:07:49 -0700 Subject: [PATCH 0011/1309] Restore API key and query param overrides (#1826) Addresses https://github.com/openai/codex/issues/1796 --- codex-rs/core/src/chat_completions.rs | 4 +- codex-rs/core/src/client.rs | 51 ++++++---------- codex-rs/core/src/model_provider_info.rs | 62 +++++++++++++------ codex-rs/core/tests/client.rs | 78 ++++++++++++++++++++++++ codex-rs/login/src/lib.rs | 2 +- 5 files changed, 144 insertions(+), 53 deletions(-) diff --git a/codex-rs/core/src/chat_completions.rs b/codex-rs/core/src/chat_completions.rs index 5ede774b1c..b1dee853f0 100644 --- a/codex-rs/core/src/chat_completions.rs +++ b/codex-rs/core/src/chat_completions.rs @@ -120,7 +120,7 @@ pub(crate) async fn stream_chat_completions( debug!( "POST to {}: {}", - provider.get_full_url(), + provider.get_full_url(&None), serde_json::to_string_pretty(&payload).unwrap_or_default() ); @@ -129,7 +129,7 @@ pub(crate) async fn stream_chat_completions( loop { attempt += 1; - let req_builder = provider.create_request_builder(client)?; + let req_builder = provider.create_request_builder(client, &None).await?; let res = req_builder .header(reqwest::header::ACCEPT, "text/event-stream") diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index b9ea6b13f4..1a8ae94f81 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -30,7 +30,6 @@ use crate::config::Config; use crate::config_types::ReasoningEffort as ReasoningEffortConfig; use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; use crate::error::CodexErr; -use crate::error::EnvVarError; use crate::error::Result; use crate::flags::CODEX_RS_SSE_FIXTURE; use crate::model_provider_info::ModelProviderInfo; @@ -122,24 +121,11 @@ impl ModelClient { return stream_from_fixture(path, self.provider.clone()).await; } - let auth = self.auth.as_ref().ok_or_else(|| { - CodexErr::EnvVar(EnvVarError { - var: "OPENAI_API_KEY".to_string(), - instructions: Some("Create an API key (https://platform.openai.com) and export it as an environment variable.".to_string()), - }) - })?; + let auth = self.auth.clone(); - let store = prompt.store && auth.mode != AuthMode::ChatGPT; + let auth_mode = auth.as_ref().map(|a| a.mode); - let base_url = match self.provider.base_url.clone() { - Some(url) => url, - None => match auth.mode { - AuthMode::ChatGPT => "https://chatgpt.com/backend-api/codex".to_string(), - AuthMode::ApiKey => "https://api.openai.com/v1".to_string(), - }, - }; - - let token = auth.get_token().await?; + let store = prompt.store && auth_mode != Some(AuthMode::ChatGPT); let full_instructions = prompt.get_full_instructions(&self.config.model); let tools_json = create_tools_json_for_responses_api( @@ -180,35 +166,36 @@ impl ModelClient { include, }; - trace!( - "POST to {}: {}", - self.provider.get_full_url(), - serde_json::to_string(&payload)? - ); - let mut attempt = 0; let max_retries = self.provider.request_max_retries(); + trace!( + "POST to {}: {}", + self.provider.get_full_url(&auth), + serde_json::to_string(&payload)? + ); + loop { attempt += 1; let mut req_builder = self - .client - .post(format!("{base_url}/responses")) + .provider + .create_request_builder(&self.client, &auth) + .await?; + + req_builder = req_builder .header("OpenAI-Beta", "responses=experimental") .header("session_id", self.session_id.to_string()) - .bearer_auth(&token) .header(reqwest::header::ACCEPT, "text/event-stream") .json(&payload); - if auth.mode == AuthMode::ChatGPT { - if let Some(account_id) = auth.get_account_id().await { - req_builder = req_builder.header("chatgpt-account-id", account_id); - } + if let Some(auth) = auth.as_ref() + && auth.mode == AuthMode::ChatGPT + && let Some(account_id) = auth.get_account_id().await + { + req_builder = req_builder.header("chatgpt-account-id", account_id); } - req_builder = self.provider.apply_http_headers(req_builder); - let originator = self .config .internal_originator diff --git a/codex-rs/core/src/model_provider_info.rs b/codex-rs/core/src/model_provider_info.rs index 2936637779..49478660f4 100644 --- a/codex-rs/core/src/model_provider_info.rs +++ b/codex-rs/core/src/model_provider_info.rs @@ -5,8 +5,11 @@ //! 2. User-defined entries inside `~/.codex/config.toml` under the `model_providers` //! key. These override or extend the defaults at runtime. +use codex_login::AuthMode; +use codex_login::CodexAuth; use serde::Deserialize; use serde::Serialize; +use std::borrow::Cow; use std::collections::HashMap; use std::env::VarError; use std::time::Duration; @@ -88,25 +91,30 @@ impl ModelProviderInfo { /// When `require_api_key` is true and the provider declares an `env_key` /// but the variable is missing/empty, returns an [`Err`] identical to the /// one produced by [`ModelProviderInfo::api_key`]. - pub fn create_request_builder<'a>( + pub async fn create_request_builder<'a>( &'a self, client: &'a reqwest::Client, + auth: &Option, ) -> crate::error::Result { - let url = self.get_full_url(); + let auth: Cow<'_, Option> = if auth.is_some() { + Cow::Borrowed(auth) + } else { + Cow::Owned(self.get_fallback_auth()?) + }; + + let url = self.get_full_url(&auth); let mut builder = client.post(url); - let api_key = self.api_key()?; - if let Some(key) = api_key { - builder = builder.bearer_auth(key); + if let Some(auth) = auth.as_ref() { + builder = builder.bearer_auth(auth.get_token().await?); } Ok(self.apply_http_headers(builder)) } - pub(crate) fn get_full_url(&self) -> String { - let query_string = self - .query_params + fn get_query_string(&self) -> String { + self.query_params .as_ref() .map_or_else(String::new, |params| { let full_params = params @@ -115,16 +123,29 @@ impl ModelProviderInfo { .collect::>() .join("&"); format!("?{full_params}") - }); + }) + } + + pub(crate) fn get_full_url(&self, auth: &Option) -> String { + let default_base_url = if matches!( + auth, + Some(CodexAuth { + mode: AuthMode::ChatGPT, + .. + }) + ) { + "https://chatgpt.com/backend-api/codex" + } else { + "https://api.openai.com/v1" + }; + let query_string = self.get_query_string(); let base_url = self .base_url .clone() - .unwrap_or("https://api.openai.com/v1".to_string()); + .unwrap_or(default_base_url.to_string()); match self.wire_api { - WireApi::Responses => { - format!("{base_url}/responses{query_string}") - } + WireApi::Responses => format!("{base_url}/responses{query_string}"), WireApi::Chat => format!("{base_url}/chat/completions{query_string}"), } } @@ -132,10 +153,7 @@ impl ModelProviderInfo { /// Apply provider-specific HTTP headers (both static and environment-based) /// onto an existing `reqwest::RequestBuilder` and return the updated /// builder. - pub fn apply_http_headers( - &self, - mut builder: reqwest::RequestBuilder, - ) -> reqwest::RequestBuilder { + fn apply_http_headers(&self, mut builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder { if let Some(extra) = &self.http_headers { for (k, v) in extra { builder = builder.header(k, v); @@ -157,7 +175,7 @@ impl ModelProviderInfo { /// If `env_key` is Some, returns the API key for this provider if present /// (and non-empty) in the environment. If `env_key` is required but /// cannot be found, returns an error. - fn api_key(&self) -> crate::error::Result> { + pub fn api_key(&self) -> crate::error::Result> { match &self.env_key { Some(env_key) => { let env_value = std::env::var(env_key); @@ -198,6 +216,14 @@ impl ModelProviderInfo { .map(Duration::from_millis) .unwrap_or(Duration::from_millis(DEFAULT_STREAM_IDLE_TIMEOUT_MS)) } + + fn get_fallback_auth(&self) -> crate::error::Result> { + let api_key = self.api_key()?; + if let Some(api_key) = api_key { + return Ok(Some(CodexAuth::from_api_key(api_key))); + } + Ok(None) + } } /// Built-in default provider list. diff --git a/codex-rs/core/tests/client.rs b/codex-rs/core/tests/client.rs index a22a94388b..06a110ea3a 100644 --- a/codex-rs/core/tests/client.rs +++ b/codex-rs/core/tests/client.rs @@ -4,6 +4,7 @@ use chrono::Utc; use codex_core::Codex; use codex_core::CodexSpawnOk; use codex_core::ModelProviderInfo; +use codex_core::WireApi; use codex_core::built_in_model_providers; use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; @@ -21,8 +22,10 @@ use tempfile::TempDir; use wiremock::Mock; use wiremock::MockServer; use wiremock::ResponseTemplate; +use wiremock::matchers::header_regex; use wiremock::matchers::method; use wiremock::matchers::path; +use wiremock::matchers::query_param; /// Build minimal SSE stream with completed marker using the JSON fixture. fn sse_completed(id: &str) -> String { @@ -376,6 +379,81 @@ async fn includes_user_instructions_message_in_request() { .starts_with("be nice") ); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn azure_overrides_assign_properties_used_for_responses_url() { + #![allow(clippy::unwrap_used)] + + let existing_env_var_with_random_value = if cfg!(windows) { "USERNAME" } else { "USER" }; + + // Mock server + let server = MockServer::start().await; + + // First request – must NOT include `previous_response_id`. + let first = ResponseTemplate::new(200) + .insert_header("content-type", "text/event-stream") + .set_body_raw(sse_completed("resp1"), "text/event-stream"); + + // Expect POST to /openai/responses with api-version query param + Mock::given(method("POST")) + .and(path("/openai/responses")) + .and(query_param("api-version", "2025-04-01-preview")) + .and(header_regex("Custom-Header", "Value")) + .and(header_regex( + "Authorization", + format!( + "Bearer {}", + std::env::var(existing_env_var_with_random_value).unwrap() + ) + .as_str(), + )) + .respond_with(first) + .expect(1) + .mount(&server) + .await; + + let provider = ModelProviderInfo { + name: "custom".to_string(), + base_url: Some(format!("{}/openai", server.uri())), + // Reuse the existing environment variable to avoid using unsafe code + env_key: Some(existing_env_var_with_random_value.to_string()), + query_params: Some(std::collections::HashMap::from([( + "api-version".to_string(), + "2025-04-01-preview".to_string(), + )])), + env_key_instructions: None, + wire_api: WireApi::Responses, + http_headers: Some(std::collections::HashMap::from([( + "Custom-Header".to_string(), + "Value".to_string(), + )])), + env_http_headers: None, + request_max_retries: None, + stream_max_retries: None, + stream_idle_timeout_ms: None, + requires_auth: false, + }; + + // Init session + let codex_home = TempDir::new().unwrap(); + let mut config = load_default_config_for_test(&codex_home); + config.model_provider = provider; + + let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new()); + let CodexSpawnOk { codex, .. } = Codex::spawn(config, None, ctrl_c.clone()).await.unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "hello".into(), + }], + }) + .await + .unwrap(); + + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; +} + fn auth_from_token(id_token: String) -> CodexAuth { CodexAuth::new( None, diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index 3d55c20264..35f67e7109 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -22,7 +22,7 @@ const SOURCE_FOR_PYTHON_SERVER: &str = include_str!("./login_with_chatgpt.py"); const CLIENT_ID: &str = "app_EMoamEEZ73f0CkXaXp7hrann"; pub const OPENAI_API_KEY_ENV_VAR: &str = "OPENAI_API_KEY"; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Copy)] pub enum AuthMode { ApiKey, ChatGPT, From f58401e203b08e64d7aff829f165d0a11c940ae7 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Mon, 4 Aug 2025 18:45:13 -0700 Subject: [PATCH 0012/1309] Request the simplified auth flow (#1834) --- codex-rs/login/src/login_with_chatgpt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/codex-rs/login/src/login_with_chatgpt.py b/codex-rs/login/src/login_with_chatgpt.py index 4c07feeba0..14ccfa9ed4 100644 --- a/codex-rs/login/src/login_with_chatgpt.py +++ b/codex-rs/login/src/login_with_chatgpt.py @@ -458,6 +458,7 @@ class _ApiKeyHTTPServer(http.server.HTTPServer): "code_challenge": self.pkce.code_challenge, "code_challenge_method": "S256", "id_token_add_organizations": "true", + "codex_cli_simplified_flow": "true", "state": self.state, } return f"{self.issuer}/oauth/authorize?" + urllib.parse.urlencode(params) From 063083af157dcf57703462c07789c54695861dff Mon Sep 17 00:00:00 2001 From: Dylan Date: Mon, 4 Aug 2025 18:55:57 -0700 Subject: [PATCH 0013/1309] [prompts] Better user_instructions handling (#1836) ## Summary Our recent change in #1737 can sometimes lead to the model confusing AGENTS.md context as part of the message. But a little prompting and formatting can help fix this! ## Testing - Ran locally with a few different prompts to verify the model behaves well. - Updated unit tests --- codex-rs/core/prompt.md | 2 ++ codex-rs/core/src/chat_completions.rs | 2 +- codex-rs/core/src/client.rs | 4 ++-- codex-rs/core/src/client_common.rs | 10 ++++++++++ codex-rs/core/tests/client.rs | 8 +++++++- 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/codex-rs/core/prompt.md b/codex-rs/core/prompt.md index 4e55003b9f..f194eba4e2 100644 --- a/codex-rs/core/prompt.md +++ b/codex-rs/core/prompt.md @@ -9,6 +9,8 @@ You MUST adhere to the following criteria when executing the task: - Analyzing code for vulnerabilities is allowed. - Showing user code and tool call details is allowed. - User instructions may overwrite the _CODING GUIDELINES_ section in this developer message. +- `user_instructions` are not part of the user's request, but guidance for how to complete the task. +- Do not cite `user_instructions` back to the user unless a specific piece is relevant. - Do not use \`ls -R\`, \`find\`, or \`grep\` - these are slow in large repos. Use \`rg\` and \`rg --files\`. - Use \`apply_patch\` to edit files: {"command":["apply_patch","*** Begin Patch\\n*** Update File: path/to/file.py\\n@@ def example():\\n- pass\\n+ return 123\\n*** End Patch"]} - If completing the user's task requires writing or modifying files: diff --git a/codex-rs/core/src/chat_completions.rs b/codex-rs/core/src/chat_completions.rs index b1dee853f0..b5ade23b9d 100644 --- a/codex-rs/core/src/chat_completions.rs +++ b/codex-rs/core/src/chat_completions.rs @@ -40,7 +40,7 @@ pub(crate) async fn stream_chat_completions( let full_instructions = prompt.get_full_instructions(model); messages.push(json!({"role": "system", "content": full_instructions})); - if let Some(instr) = &prompt.user_instructions { + if let Some(instr) = &prompt.get_formatted_user_instructions() { messages.push(json!({"role": "user", "content": instr})); } diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 1a8ae94f81..00762a8a67 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -144,11 +144,11 @@ impl ModelClient { }; let mut input_with_instructions = Vec::with_capacity(prompt.input.len() + 1); - if let Some(ui) = &prompt.user_instructions { + if let Some(ui) = prompt.get_formatted_user_instructions() { input_with_instructions.push(ResponseItem::Message { id: None, role: "user".to_string(), - content: vec![ContentItem::InputText { text: ui.clone() }], + content: vec![ContentItem::InputText { text: ui }], }); } input_with_instructions.extend(prompt.input.clone()); diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index 157f35872a..6d9524cc92 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -17,6 +17,10 @@ use tokio::sync::mpsc; /// with this content. const BASE_INSTRUCTIONS: &str = include_str!("../prompt.md"); +/// wraps user instructions message in a tag for the model to parse more easily. +const USER_INSTRUCTIONS_START: &str = "\n\n"; +const USER_INSTRUCTIONS_END: &str = "\n\n"; + /// API request payload for a single model turn. #[derive(Default, Debug, Clone)] pub struct Prompt { @@ -49,6 +53,12 @@ impl Prompt { } Cow::Owned(sections.join("\n")) } + + pub(crate) fn get_formatted_user_instructions(&self) -> Option { + self.user_instructions + .as_ref() + .map(|ui| format!("{USER_INSTRUCTIONS_START}{ui}{USER_INSTRUCTIONS_END}")) + } } #[derive(Debug)] diff --git a/codex-rs/core/tests/client.rs b/codex-rs/core/tests/client.rs index 06a110ea3a..f493020210 100644 --- a/codex-rs/core/tests/client.rs +++ b/codex-rs/core/tests/client.rs @@ -376,7 +376,13 @@ async fn includes_user_instructions_message_in_request() { request_body["input"][0]["content"][0]["text"] .as_str() .unwrap() - .starts_with("be nice") + .starts_with("\n\nbe nice") + ); + assert!( + request_body["input"][0]["content"][0]["text"] + .as_str() + .unwrap() + .ends_with("") ); } From 906d44976001aa6fbd51c9a40129e20b92e819ce Mon Sep 17 00:00:00 2001 From: easong-openai Date: Mon, 4 Aug 2025 21:23:22 -0700 Subject: [PATCH 0014/1309] Stream model responses (#1810) Stream models thoughts and responses instead of waiting for the whole thing to come through. Very rough right now, but I'm making the risk call to push through. --- codex-rs/Cargo.lock | 50 ++- codex-rs/core/src/chat_completions.rs | 18 +- codex-rs/core/src/codex.rs | 12 +- codex-rs/core/src/conversation_history.rs | 186 +++++++- codex-rs/core/src/models.rs | 17 +- codex-rs/tui/Cargo.toml | 5 + .../tui/src/bottom_pane/live_ring_widget.rs | 45 ++ codex-rs/tui/src/bottom_pane/mod.rs | 399 ++++++++++++++++-- codex-rs/tui/src/chatwidget.rs | 174 ++++++-- codex-rs/tui/src/history_cell.rs | 32 +- codex-rs/tui/src/insert_history.rs | 81 +++- codex-rs/tui/src/lib.rs | 5 +- codex-rs/tui/src/live_wrap.rs | 290 +++++++++++++ codex-rs/tui/src/markdown.rs | 6 +- codex-rs/tui/src/status_indicator_widget.rs | 215 +++++++--- codex-rs/tui/tests/vt100_history.rs | 214 ++++++++++ codex-rs/tui/tests/vt100_live_commit.rs | 101 +++++ 17 files changed, 1616 insertions(+), 234 deletions(-) create mode 100644 codex-rs/tui/src/bottom_pane/live_ring_widget.rs create mode 100644 codex-rs/tui/src/live_wrap.rs create mode 100644 codex-rs/tui/tests/vt100_history.rs create mode 100644 codex-rs/tui/tests/vt100_live_commit.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 4daae977b0..2e20a7d624 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -881,6 +881,7 @@ dependencies = [ "unicode-segmentation", "unicode-width 0.1.14", "uuid", + "vt100", ] [[package]] @@ -1473,7 +1474,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -1553,7 +1554,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", "rustix 1.0.8", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1756,7 +1757,7 @@ version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" dependencies = [ - "unicode-width 0.2.0", + "unicode-width 0.2.1", ] [[package]] @@ -2336,7 +2337,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3392,7 +3393,7 @@ dependencies = [ [[package]] name = "ratatui" version = "0.29.0" -source = "git+https://github.com/nornagon/ratatui?branch=nornagon-v0.29.0-patch#bca287ddc5d38fe088c79e2eda22422b96226f2e" +source = "git+https://github.com/nornagon/ratatui?branch=nornagon-v0.29.0-patch#9b2ad1298408c45918ee9f8241a6f95498cdbed2" dependencies = [ "bitflags 2.9.1", "cassowary", @@ -3406,7 +3407,7 @@ dependencies = [ "strum 0.26.3", "unicode-segmentation", "unicode-truncate", - "unicode-width 0.2.0", + "unicode-width 0.2.1", ] [[package]] @@ -3720,7 +3721,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3733,7 +3734,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -4499,7 +4500,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix 1.0.8", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -4546,7 +4547,7 @@ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ "smawk", "unicode-linebreak", - "unicode-width 0.2.0", + "unicode-width 0.2.1", ] [[package]] @@ -4994,7 +4995,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "911e93158bf80bbc94bad533b2b16e3d711e1132d69a6a6980c3920a63422c19" dependencies = [ "ratatui", - "unicode-width 0.2.0", + "unicode-width 0.2.1", ] [[package]] @@ -5062,9 +5063,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] name = "unicode-xid" @@ -5149,6 +5150,27 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vt100" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ff75fb8fa83e609e685106df4faeffdf3a735d3c74ebce97ec557d5d36fd9" +dependencies = [ + "itoa", + "unicode-width 0.2.1", + "vte", +] + +[[package]] +name = "vte" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd" +dependencies = [ + "arrayvec", + "memchr", +] + [[package]] name = "wait-timeout" version = "0.2.1" @@ -5337,7 +5359,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/codex-rs/core/src/chat_completions.rs b/codex-rs/core/src/chat_completions.rs index b5ade23b9d..e1804b191e 100644 --- a/codex-rs/core/src/chat_completions.rs +++ b/codex-rs/core/src/chat_completions.rs @@ -260,6 +260,11 @@ async fn process_chat_sse( .and_then(|d| d.get("content")) .and_then(|c| c.as_str()) { + // Emit a delta so downstream consumers can stream text live. + let _ = tx_event + .send(Ok(ResponseEvent::OutputTextDelta(content.to_string()))) + .await; + let item = ResponseItem::Message { role: "assistant".to_string(), content: vec![ContentItem::OutputText { @@ -439,11 +444,14 @@ where // will never appear in a Chat Completions stream. continue; } - Poll::Ready(Some(Ok(ResponseEvent::OutputTextDelta(_)))) - | Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryDelta(_)))) => { - // Deltas are ignored here since aggregation waits for the - // final OutputItemDone. - continue; + Poll::Ready(Some(Ok(ResponseEvent::OutputTextDelta(delta)))) => { + // Forward deltas unchanged so callers can stream text + // live while still receiving a single aggregated + // OutputItemDone at the end of the turn. + return Poll::Ready(Some(Ok(ResponseEvent::OutputTextDelta(delta)))); + } + Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryDelta(delta)))) => { + return Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryDelta(delta)))); } } } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 568d87c4a8..8d24356460 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -123,7 +123,7 @@ impl Codex { let resume_path = config.experimental_resume.clone(); info!("resume_path: {resume_path:?}"); let (tx_sub, rx_sub) = async_channel::bounded(64); - let (tx_event, rx_event) = async_channel::bounded(1600); + let (tx_event, rx_event) = async_channel::unbounded(); let user_instructions = get_user_instructions(&config).await; @@ -701,7 +701,7 @@ async fn submission_loop( cwd, resume_path, } => { - info!( + debug!( "Configuring session: model={model}; provider={provider:?}; resume={resume_path:?}" ); if !cwd.is_absolute() { @@ -1374,6 +1374,11 @@ async fn try_run_turn( return Ok(output); } ResponseEvent::OutputTextDelta(delta) => { + { + let mut st = sess.state.lock().unwrap(); + st.history.append_assistant_text(&delta); + } + let event = Event { id: sub_id.to_string(), msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta }), @@ -1921,7 +1926,8 @@ async fn handle_sandbox_error( // include additional metadata on the command to indicate whether non-zero // exit codes merit a retry. - // For now, we categorically ask the user to retry without sandbox. + // For now, we categorically ask the user to retry without sandbox and + // emit the raw error as a background event. sess.notify_background_event(&sub_id, format!("Execution failed: {error}")) .await; diff --git a/codex-rs/core/src/conversation_history.rs b/codex-rs/core/src/conversation_history.rs index f5254f339e..1d55b125bc 100644 --- a/codex-rs/core/src/conversation_history.rs +++ b/codex-rs/core/src/conversation_history.rs @@ -24,9 +24,52 @@ impl ConversationHistory { I::Item: std::ops::Deref, { for item in items { - if is_api_message(&item) { - // Note agent-loop.ts also does filtering on some of the fields. - self.items.push(item.clone()); + if !is_api_message(&item) { + continue; + } + + // Merge adjacent assistant messages into a single history entry. + // This prevents duplicates when a partial assistant message was + // streamed into history earlier in the turn and the final full + // message is recorded at turn end. + match (&*item, self.items.last_mut()) { + ( + ResponseItem::Message { + role: new_role, + content: new_content, + .. + }, + Some(ResponseItem::Message { + role: last_role, + content: last_content, + .. + }), + ) if new_role == "assistant" && last_role == "assistant" => { + append_text_content(last_content, new_content); + } + _ => { + self.items.push(item.clone()); + } + } + } + } + + /// Append a text `delta` to the latest assistant message, creating a new + /// assistant entry if none exists yet (e.g. first delta for this turn). + pub(crate) fn append_assistant_text(&mut self, delta: &str) { + match self.items.last_mut() { + Some(ResponseItem::Message { role, content, .. }) if role == "assistant" => { + append_text_delta(content, delta); + } + _ => { + // Start a new assistant message with the delta. + self.items.push(ResponseItem::Message { + id: None, + role: "assistant".to_string(), + content: vec![crate::models::ContentItem::OutputText { + text: delta.to_string(), + }], + }); } } } @@ -72,3 +115,140 @@ fn is_api_message(message: &ResponseItem) -> bool { ResponseItem::Other => false, } } + +/// Helper to append the textual content from `src` into `dst` in place. +fn append_text_content( + dst: &mut Vec, + src: &Vec, +) { + for c in src { + if let crate::models::ContentItem::OutputText { text } = c { + append_text_delta(dst, text); + } + } +} + +/// Append a single text delta to the last OutputText item in `content`, or +/// push a new OutputText item if none exists. +fn append_text_delta(content: &mut Vec, delta: &str) { + if let Some(crate::models::ContentItem::OutputText { text }) = content + .iter_mut() + .rev() + .find(|c| matches!(c, crate::models::ContentItem::OutputText { .. })) + { + text.push_str(delta); + } else { + content.push(crate::models::ContentItem::OutputText { + text: delta.to_string(), + }); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::ContentItem; + + fn assistant_msg(text: &str) -> ResponseItem { + ResponseItem::Message { + id: None, + role: "assistant".to_string(), + content: vec![ContentItem::OutputText { + text: text.to_string(), + }], + } + } + + fn user_msg(text: &str) -> ResponseItem { + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::OutputText { + text: text.to_string(), + }], + } + } + + #[test] + fn merges_adjacent_assistant_messages() { + let mut h = ConversationHistory::default(); + let a1 = assistant_msg("Hello"); + let a2 = assistant_msg(", world!"); + h.record_items([&a1, &a2]); + + let items = h.contents(); + assert_eq!( + items, + vec![ResponseItem::Message { + id: None, + role: "assistant".to_string(), + content: vec![ContentItem::OutputText { + text: "Hello, world!".to_string() + }] + }] + ); + } + + #[test] + fn append_assistant_text_creates_and_appends() { + let mut h = ConversationHistory::default(); + h.append_assistant_text("Hello"); + h.append_assistant_text(", world"); + + // Now record a final full assistant message and verify it merges. + let final_msg = assistant_msg("!"); + h.record_items([&final_msg]); + + let items = h.contents(); + assert_eq!( + items, + vec![ResponseItem::Message { + id: None, + role: "assistant".to_string(), + content: vec![ContentItem::OutputText { + text: "Hello, world!".to_string() + }] + }] + ); + } + + #[test] + fn filters_non_api_messages() { + let mut h = ConversationHistory::default(); + // System message is not an API message; Other is ignored. + let system = ResponseItem::Message { + id: None, + role: "system".to_string(), + content: vec![ContentItem::OutputText { + text: "ignored".to_string(), + }], + }; + h.record_items([&system, &ResponseItem::Other]); + + // User and assistant should be retained. + let u = user_msg("hi"); + let a = assistant_msg("hello"); + h.record_items([&u, &a]); + + let items = h.contents(); + assert_eq!( + items, + vec![ + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::OutputText { + text: "hi".to_string() + }] + }, + ResponseItem::Message { + id: None, + role: "assistant".to_string(), + content: vec![ContentItem::OutputText { + text: "hello".to_string() + }] + } + ] + ); + } +} diff --git a/codex-rs/core/src/models.rs b/codex-rs/core/src/models.rs index 166404915a..91bfb3bc8c 100644 --- a/codex-rs/core/src/models.rs +++ b/codex-rs/core/src/models.rs @@ -9,7 +9,7 @@ use serde::ser::Serializer; use crate::protocol::InputItem; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ResponseInputItem { Message { @@ -26,7 +26,7 @@ pub enum ResponseInputItem { }, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ContentItem { InputText { text: String }, @@ -34,7 +34,7 @@ pub enum ContentItem { OutputText { text: String }, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ResponseItem { Message { @@ -107,7 +107,7 @@ impl From for ResponseItem { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum LocalShellStatus { Completed, @@ -115,13 +115,13 @@ pub enum LocalShellStatus { Incomplete, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(tag = "type", rename_all = "snake_case")] pub enum LocalShellAction { Exec(LocalShellExecAction), } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct LocalShellExecAction { pub command: Vec, pub timeout_ms: Option, @@ -130,7 +130,7 @@ pub struct LocalShellExecAction { pub user: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ReasoningItemReasoningSummary { SummaryText { text: String }, @@ -185,10 +185,9 @@ pub struct ShellToolCallParams { pub timeout_ms: Option, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct FunctionCallOutputPayload { pub content: String, - #[expect(dead_code)] pub success: Option, } diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index a571b32c8d..60af056a2d 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -11,6 +11,10 @@ path = "src/main.rs" name = "codex_tui" path = "src/lib.rs" +[features] +# Enable vt100-based tests (emulator) when running with `--features vt100-tests`. +vt100-tests = [] + [lints] workspace = true @@ -73,3 +77,4 @@ insta = "1.43.1" pretty_assertions = "1" rand = "0.8" chrono = { version = "0.4", features = ["serde"] } +vt100 = "0.16.2" diff --git a/codex-rs/tui/src/bottom_pane/live_ring_widget.rs b/codex-rs/tui/src/bottom_pane/live_ring_widget.rs new file mode 100644 index 0000000000..13f91acc5d --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/live_ring_widget.rs @@ -0,0 +1,45 @@ +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::text::Line; +use ratatui::widgets::Paragraph; +use ratatui::widgets::WidgetRef; + +/// Minimal rendering-only widget for the transient ring rows. +pub(crate) struct LiveRingWidget { + max_rows: u16, + rows: Vec>, // newest at the end +} + +impl LiveRingWidget { + pub fn new() -> Self { + Self { + max_rows: 3, + rows: Vec::new(), + } + } + + pub fn set_max_rows(&mut self, n: u16) { + self.max_rows = n.max(1); + } + + pub fn set_rows(&mut self, rows: Vec>) { + self.rows = rows; + } + + pub fn desired_height(&self, _width: u16) -> u16 { + let len = self.rows.len() as u16; + len.min(self.max_rows) + } +} + +impl WidgetRef for LiveRingWidget { + fn render_ref(&self, area: Rect, buf: &mut Buffer) { + if area.height == 0 { + return; + } + let visible = self.rows.len().saturating_sub(self.max_rows as usize); + let slice = &self.rows[visible..]; + let para = Paragraph::new(slice.to_vec()); + para.render_ref(area, buf); + } +} diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index cab78bbe3f..fde0b3bde8 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -4,12 +4,12 @@ use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; use crate::user_approval_widget::ApprovalRequest; use bottom_pane_view::BottomPaneView; -use bottom_pane_view::ConditionalUpdate; use codex_core::protocol::TokenUsage; use codex_file_search::FileMatch; use crossterm::event::KeyEvent; use ratatui::buffer::Buffer; use ratatui::layout::Rect; +use ratatui::text::Line; use ratatui::widgets::WidgetRef; mod approval_modal_view; @@ -18,6 +18,7 @@ mod chat_composer; mod chat_composer_history; mod command_popup; mod file_search_popup; +mod live_ring_widget; mod status_indicator_view; mod textarea; @@ -30,6 +31,7 @@ pub(crate) enum CancellationEvent { pub(crate) use chat_composer::ChatComposer; pub(crate) use chat_composer::InputResult; +use crate::status_indicator_widget::StatusIndicatorWidget; use approval_modal_view::ApprovalModalView; use status_indicator_view::StatusIndicatorView; @@ -46,6 +48,19 @@ pub(crate) struct BottomPane<'a> { has_input_focus: bool, is_task_running: bool, ctrl_c_quit_hint: bool, + + /// Optional live, multi‑line status/"live cell" rendered directly above + /// the composer while a task is running. Unlike `active_view`, this does + /// not replace the composer; it augments it. + live_status: Option, + + /// Optional transient ring shown above the composer. This is a rendering-only + /// container used during development before we wire it to ChatWidget events. + live_ring: Option, + + /// True if the active view is the StatusIndicatorView that replaces the + /// composer during a running task. + status_view_active: bool, } pub(crate) struct BottomPaneParams { @@ -55,6 +70,7 @@ pub(crate) struct BottomPaneParams { } impl BottomPane<'_> { + const BOTTOM_PAD_LINES: u16 = 2; pub fn new(params: BottomPaneParams) -> Self { let enhanced_keys_supported = params.enhanced_keys_supported; Self { @@ -68,14 +84,40 @@ impl BottomPane<'_> { has_input_focus: params.has_input_focus, is_task_running: false, ctrl_c_quit_hint: false, + live_status: None, + live_ring: None, + status_view_active: false, } } pub fn desired_height(&self, width: u16) -> u16 { - self.active_view + let overlay_status_h = self + .live_status .as_ref() - .map(|v| v.desired_height(width)) - .unwrap_or(self.composer.desired_height(width)) + .map(|s| s.desired_height(width)) + .unwrap_or(0); + let ring_h = self + .live_ring + .as_ref() + .map(|r| r.desired_height(width)) + .unwrap_or(0); + + let view_height = if let Some(view) = self.active_view.as_ref() { + // Add a single blank spacer line between live ring and status view when active. + let spacer = if self.live_ring.is_some() && self.status_view_active { + 1 + } else { + 0 + }; + spacer + view.desired_height(width) + } else { + self.composer.desired_height(width) + }; + + overlay_status_h + .saturating_add(ring_h) + .saturating_add(view_height) + .saturating_add(Self::BOTTOM_PAD_LINES) } pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> { @@ -96,10 +138,6 @@ impl BottomPane<'_> { view.handle_key_event(self, key_event); if !view.is_complete() { self.active_view = Some(view); - } else if self.is_task_running { - self.active_view = Some(Box::new(StatusIndicatorView::new( - self.app_event_tx.clone(), - ))); } self.request_redraw(); InputResult::None @@ -125,10 +163,6 @@ impl BottomPane<'_> { CancellationEvent::Handled => { if !view.is_complete() { self.active_view = Some(view); - } else if self.is_task_running { - self.active_view = Some(Box::new(StatusIndicatorView::new( - self.app_event_tx.clone(), - ))); } self.show_ctrl_c_quit_hint(); } @@ -148,19 +182,37 @@ impl BottomPane<'_> { } } - /// Update the status indicator text (only when the `StatusIndicatorView` is - /// active). + /// Update the status indicator text. Prefer replacing the composer with + /// the StatusIndicatorView so the input pane shows a single-line status + /// like: `▌ Working waiting for model`. pub(crate) fn update_status_text(&mut self, text: String) { - if let Some(view) = &mut self.active_view { - match view.update_status_text(text) { - ConditionalUpdate::NeedsRedraw => { - self.request_redraw(); - } - ConditionalUpdate::NoRedraw => { - // No redraw needed. - } + let mut handled_by_view = false; + if let Some(view) = self.active_view.as_mut() { + if matches!( + view.update_status_text(text.clone()), + bottom_pane_view::ConditionalUpdate::NeedsRedraw + ) { + handled_by_view = true; + } + } else { + let mut v = StatusIndicatorView::new(self.app_event_tx.clone()); + v.update_text(text.clone()); + self.active_view = Some(Box::new(v)); + self.status_view_active = true; + handled_by_view = true; + } + + // Fallback: if the current active view did not consume status updates, + // present an overlay above the composer. + if !handled_by_view { + if self.live_status.is_none() { + self.live_status = Some(StatusIndicatorWidget::new(self.app_event_tx.clone())); + } + if let Some(status) = &mut self.live_status { + status.update_text(text); } } + self.request_redraw(); } pub(crate) fn show_ctrl_c_quit_hint(&mut self) { @@ -186,27 +238,23 @@ impl BottomPane<'_> { pub fn set_task_running(&mut self, running: bool) { self.is_task_running = running; - match (running, self.active_view.is_some()) { - (true, false) => { - // Show status indicator overlay. + if running { + if self.active_view.is_none() { self.active_view = Some(Box::new(StatusIndicatorView::new( self.app_event_tx.clone(), ))); - self.request_redraw(); + self.status_view_active = true; } - (false, true) => { - if let Some(mut view) = self.active_view.take() { - if view.should_hide_when_task_is_done() { - // Leave self.active_view as None. - self.request_redraw(); - } else { - // Preserve the view. - self.active_view = Some(view); - } + self.request_redraw(); + } else { + self.live_status = None; + // Drop the status view when a task completes, but keep other + // modal views (e.g. approval dialogs). + if let Some(mut view) = self.active_view.take() { + if !view.should_hide_when_task_is_done() { + self.active_view = Some(view); } - } - _ => { - // No change. + self.status_view_active = false; } } } @@ -248,6 +296,7 @@ impl BottomPane<'_> { // Otherwise create a new approval modal overlay. let modal = ApprovalModalView::new(request, self.app_event_tx.clone()); self.active_view = Some(Box::new(modal)); + self.status_view_active = false; self.request_redraw() } @@ -281,15 +330,80 @@ impl BottomPane<'_> { self.composer.on_file_search_result(query, matches); self.request_redraw(); } + + /// Set the rows and cap for the transient live ring overlay. + pub(crate) fn set_live_ring_rows(&mut self, max_rows: u16, rows: Vec>) { + let mut w = live_ring_widget::LiveRingWidget::new(); + w.set_max_rows(max_rows); + w.set_rows(rows); + self.live_ring = Some(w); + } + + pub(crate) fn clear_live_ring(&mut self) { + self.live_ring = None; + } + + // Removed restart_live_status_with_text – no longer used by the current streaming UI. } impl WidgetRef for &BottomPane<'_> { fn render_ref(&self, area: Rect, buf: &mut Buffer) { - // Show BottomPaneView if present. - if let Some(ov) = &self.active_view { - ov.render(area, buf); - } else { - (&self.composer).render_ref(area, buf); + let mut y_offset = 0u16; + if let Some(ring) = &self.live_ring { + let live_h = ring.desired_height(area.width).min(area.height); + if live_h > 0 { + let live_rect = Rect { + x: area.x, + y: area.y, + width: area.width, + height: live_h, + }; + ring.render_ref(live_rect, buf); + y_offset = live_h; + } + } + // Spacer between live ring and status view when active + if self.live_ring.is_some() && self.status_view_active && y_offset < area.height { + // Leave one empty line + y_offset = y_offset.saturating_add(1); + } + if let Some(status) = &self.live_status { + let live_h = status.desired_height(area.width).min(area.height); + if live_h > 0 { + let live_rect = Rect { + x: area.x, + y: area.y, + width: area.width, + height: live_h, + }; + status.render_ref(live_rect, buf); + y_offset = live_h; + } + } + + if let Some(view) = &self.active_view { + if y_offset < area.height { + // Reserve bottom padding lines; keep at least 1 line for the view. + let avail = area.height - y_offset; + let pad = BottomPane::BOTTOM_PAD_LINES.min(avail.saturating_sub(1)); + let view_rect = Rect { + x: area.x, + y: area.y + y_offset, + width: area.width, + height: avail - pad, + }; + view.render(view_rect, buf); + } + } else if y_offset < area.height { + let composer_rect = Rect { + x: area.x, + y: area.y + y_offset, + width: area.width, + // Reserve bottom padding + height: (area.height - y_offset) + - BottomPane::BOTTOM_PAD_LINES.min((area.height - y_offset).saturating_sub(1)), + }; + (&self.composer).render_ref(composer_rect, buf); } } } @@ -298,6 +412,9 @@ impl WidgetRef for &BottomPane<'_> { mod tests { use super::*; use crate::app_event::AppEvent; + use ratatui::buffer::Buffer; + use ratatui::layout::Rect; + use ratatui::text::Line; use std::path::PathBuf; use std::sync::mpsc::channel; @@ -324,4 +441,200 @@ mod tests { assert!(pane.ctrl_c_quit_hint_visible()); assert_eq!(CancellationEvent::Ignored, pane.on_ctrl_c()); } + + #[test] + fn live_ring_renders_above_composer() { + let (tx_raw, _rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let mut pane = BottomPane::new(BottomPaneParams { + app_event_tx: tx, + has_input_focus: true, + enhanced_keys_supported: false, + }); + + // Provide 4 rows with max_rows=3; only the last 3 should be visible. + pane.set_live_ring_rows( + 3, + vec![ + Line::from("one".to_string()), + Line::from("two".to_string()), + Line::from("three".to_string()), + Line::from("four".to_string()), + ], + ); + + let area = Rect::new(0, 0, 10, 5); + let mut buf = Buffer::empty(area); + (&pane).render_ref(area, &mut buf); + + // Extract the first 3 rows and assert they contain the last three lines. + let mut lines: Vec = Vec::new(); + for y in 0..3 { + let mut s = String::new(); + for x in 0..area.width { + s.push(buf[(x, y)].symbol().chars().next().unwrap_or(' ')); + } + lines.push(s.trim_end().to_string()); + } + assert_eq!(lines, vec!["two", "three", "four"]); + } + + #[test] + fn status_indicator_visible_with_live_ring() { + let (tx_raw, _rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let mut pane = BottomPane::new(BottomPaneParams { + app_event_tx: tx, + has_input_focus: true, + enhanced_keys_supported: false, + }); + + // Simulate task running which replaces composer with the status indicator. + pane.set_task_running(true); + pane.update_status_text("waiting for model".to_string()); + + // Provide 2 rows in the live ring (e.g., streaming CoT) and ensure the + // status indicator remains visible below them. + pane.set_live_ring_rows( + 2, + vec![ + Line::from("cot1".to_string()), + Line::from("cot2".to_string()), + ], + ); + + // Allow some frames so the dot animation is present. + std::thread::sleep(std::time::Duration::from_millis(120)); + + // Height should include both ring rows, 1 spacer, and the 1-line status. + let area = Rect::new(0, 0, 30, 4); + let mut buf = Buffer::empty(area); + (&pane).render_ref(area, &mut buf); + + // Top two rows are the live ring. + let mut r0 = String::new(); + let mut r1 = String::new(); + for x in 0..area.width { + r0.push(buf[(x, 0)].symbol().chars().next().unwrap_or(' ')); + r1.push(buf[(x, 1)].symbol().chars().next().unwrap_or(' ')); + } + assert!(r0.contains("cot1"), "expected first live row: {r0:?}"); + assert!(r1.contains("cot2"), "expected second live row: {r1:?}"); + + // Row 2 is the spacer (blank) + let mut r2 = String::new(); + for x in 0..area.width { + r2.push(buf[(x, 2)].symbol().chars().next().unwrap_or(' ')); + } + assert!(r2.trim().is_empty(), "expected blank spacer line: {r2:?}"); + + // Bottom row is the status line; it should contain the left bar and "Working". + let mut r3 = String::new(); + for x in 0..area.width { + r3.push(buf[(x, 3)].symbol().chars().next().unwrap_or(' ')); + } + assert_eq!(buf[(0, 3)].symbol().chars().next().unwrap_or(' '), '▌'); + assert!( + r3.contains("Working"), + "expected Working header in status line: {r3:?}" + ); + } + + #[test] + fn bottom_padding_present_for_status_view() { + let (tx_raw, _rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let mut pane = BottomPane::new(BottomPaneParams { + app_event_tx: tx, + has_input_focus: true, + enhanced_keys_supported: false, + }); + + // Activate spinner (status view replaces composer) with no live ring. + pane.set_task_running(true); + pane.update_status_text("waiting for model".to_string()); + + // Use height == desired_height; expect 1 status row at top and 2 bottom padding rows. + let height = pane.desired_height(30); + assert!( + height >= 3, + "expected at least 3 rows with bottom padding; got {height}" + ); + let area = Rect::new(0, 0, 30, height); + let mut buf = Buffer::empty(area); + (&pane).render_ref(area, &mut buf); + + // Top row contains the status header + let mut top = String::new(); + for x in 0..area.width { + top.push(buf[(x, 0)].symbol().chars().next().unwrap_or(' ')); + } + assert_eq!(buf[(0, 0)].symbol().chars().next().unwrap_or(' '), '▌'); + assert!( + top.contains("Working"), + "expected Working header on top row: {top:?}" + ); + + // Bottom two rows are blank padding + let mut r_last = String::new(); + let mut r_last2 = String::new(); + for x in 0..area.width { + r_last.push(buf[(x, height - 1)].symbol().chars().next().unwrap_or(' ')); + r_last2.push(buf[(x, height - 2)].symbol().chars().next().unwrap_or(' ')); + } + assert!( + r_last.trim().is_empty(), + "expected last row blank: {r_last:?}" + ); + assert!( + r_last2.trim().is_empty(), + "expected second-to-last row blank: {r_last2:?}" + ); + } + + #[test] + fn bottom_padding_shrinks_when_tiny() { + let (tx_raw, _rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let mut pane = BottomPane::new(BottomPaneParams { + app_event_tx: tx, + has_input_focus: true, + enhanced_keys_supported: false, + }); + + pane.set_task_running(true); + pane.update_status_text("waiting for model".to_string()); + + // Height=2 → pad shrinks to 1; bottom row is blank, top row has spinner. + let area2 = Rect::new(0, 0, 20, 2); + let mut buf2 = Buffer::empty(area2); + (&pane).render_ref(area2, &mut buf2); + let mut row0 = String::new(); + let mut row1 = String::new(); + for x in 0..area2.width { + row0.push(buf2[(x, 0)].symbol().chars().next().unwrap_or(' ')); + row1.push(buf2[(x, 1)].symbol().chars().next().unwrap_or(' ')); + } + assert!( + row0.contains("Working"), + "expected Working header on row 0: {row0:?}" + ); + assert!( + row1.trim().is_empty(), + "expected bottom padding on row 1: {row1:?}" + ); + + // Height=1 → no padding; single row is the spinner. + let area1 = Rect::new(0, 0, 20, 1); + let mut buf1 = Buffer::empty(area1); + (&pane).render_ref(area1, &mut buf1); + let mut only = String::new(); + for x in 0..area1.width { + only.push(buf1[(x, 0)].symbol().chars().next().unwrap_or(' ')); + } + assert!( + only.contains("Working"), + "expected Working header with no padding: {only:?}" + ); + } } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index e5ebf58a07..f63810b62a 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -42,8 +42,10 @@ use crate::exec_command::strip_bash_lc_and_escape; use crate::history_cell::CommandOutput; use crate::history_cell::HistoryCell; use crate::history_cell::PatchEventType; +use crate::live_wrap::RowBuilder; use crate::user_approval_widget::ApprovalRequest; use codex_file_search::FileMatch; +use ratatui::style::Stylize; struct RunningCommand { command: Vec, @@ -64,6 +66,10 @@ pub(crate) struct ChatWidget<'a> { // at once into scrollback so the history contains a single message. answer_buffer: String, running_commands: HashMap, + live_builder: RowBuilder, + current_stream: Option, + stream_header_emitted: bool, + live_max_rows: u16, } struct UserMessage { @@ -71,6 +77,12 @@ struct UserMessage { image_paths: Vec, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum StreamKind { + Answer, + Reasoning, +} + impl From for UserMessage { fn from(text: String) -> Self { Self { @@ -151,6 +163,10 @@ impl ChatWidget<'_> { reasoning_buffer: String::new(), answer_buffer: String::new(), running_commands: HashMap::new(), + live_builder: RowBuilder::new(80), + current_stream: None, + stream_header_emitted: false, + live_max_rows: 3, } } @@ -234,58 +250,45 @@ impl ChatWidget<'_> { self.request_redraw(); } - EventMsg::AgentMessage(AgentMessageEvent { message }) => { - // Final assistant answer. Prefer the fully provided message - // from the event; if it is empty fall back to any accumulated - // delta buffer (some providers may only stream deltas and send - // an empty final message). - let full = if message.is_empty() { - std::mem::take(&mut self.answer_buffer) - } else { - self.answer_buffer.clear(); - message - }; - if !full.is_empty() { - self.add_to_history(HistoryCell::new_agent_message(&self.config, full)); - } + EventMsg::AgentMessage(AgentMessageEvent { message: _ }) => { + // Final assistant answer: commit all remaining rows and close with + // a blank line. Use the final text if provided, otherwise rely on + // streamed deltas already in the builder. + self.finalize_stream(StreamKind::Answer); self.request_redraw(); } EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta }) => { - // Buffer only – do not emit partial lines. This avoids cases - // where long responses appear truncated if the terminal - // wrapped early. The full message is emitted on - // AgentMessage. + self.begin_stream(StreamKind::Answer); self.answer_buffer.push_str(&delta); + self.stream_push_and_maybe_commit(&delta); + self.request_redraw(); } EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta }) => { - // Buffer only – disable incremental reasoning streaming so we - // avoid truncated intermediate lines. Full text emitted on - // AgentReasoning. + // Stream CoT into the live pane; keep input visible and commit + // overflow rows incrementally to scrollback. + self.begin_stream(StreamKind::Reasoning); self.reasoning_buffer.push_str(&delta); + self.stream_push_and_maybe_commit(&delta); + self.request_redraw(); } - EventMsg::AgentReasoning(AgentReasoningEvent { text }) => { - // Emit full reasoning text once. Some providers might send - // final event with empty text if only deltas were used. - let full = if text.is_empty() { - std::mem::take(&mut self.reasoning_buffer) - } else { - self.reasoning_buffer.clear(); - text - }; - if !full.is_empty() { - self.add_to_history(HistoryCell::new_agent_reasoning(&self.config, full)); - } + EventMsg::AgentReasoning(AgentReasoningEvent { text: _ }) => { + // Final reasoning: commit remaining rows and close with a blank. + self.finalize_stream(StreamKind::Reasoning); self.request_redraw(); } EventMsg::TaskStarted => { self.bottom_pane.clear_ctrl_c_quit_hint(); self.bottom_pane.set_task_running(true); + // Replace composer with single-line spinner while waiting. + self.bottom_pane + .update_status_text("waiting for model".to_string()); self.request_redraw(); } EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message: _, }) => { self.bottom_pane.set_task_running(false); + self.bottom_pane.clear_live_ring(); self.request_redraw(); } EventMsg::TokenCount(token_usage) => { @@ -298,8 +301,8 @@ impl ChatWidget<'_> { self.bottom_pane.set_task_running(false); } EventMsg::PlanUpdate(update) => { + // Commit plan updates directly to history (no status-line preview). self.add_to_history(HistoryCell::new_plan_update(update)); - self.request_redraw(); } EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { call_id: _, @@ -307,8 +310,7 @@ impl ChatWidget<'_> { cwd, reason, }) => { - // Print the command to the history so it is visible in the - // transcript *before* the modal asks for approval. + // Log a background summary immediately so the history is chronological. let cmdline = strip_bash_lc_and_escape(&command); let text = format!( "command requires approval:\n$ {cmdline}{reason}", @@ -344,7 +346,6 @@ impl ChatWidget<'_> { // approval dialog) and avoids surprising the user with a modal // prompt before they have seen *what* is being requested. // ------------------------------------------------------------------ - self.add_to_history(HistoryCell::new_patch_event( PatchEventType::ApprovalRequest, changes, @@ -379,8 +380,6 @@ impl ChatWidget<'_> { auto_approved, changes, }) => { - // Even when a patch is auto‑approved we still display the - // summary so the user can follow along. self.add_to_history(HistoryCell::new_patch_event( PatchEventType::ApplyBegin { auto_approved }, changes, @@ -393,6 +392,7 @@ impl ChatWidget<'_> { stdout, stderr, }) => { + // Compute summary before moving stdout into the history cell. let cmd = self.running_commands.remove(&call_id); self.add_to_history(HistoryCell::new_completed_exec_command( cmd.map(|cmd| cmd.command).unwrap_or_else(|| vec![call_id]), @@ -442,14 +442,15 @@ impl ChatWidget<'_> { self.app_event_tx.send(AppEvent::ExitRequest); } event => { - self.add_to_history(HistoryCell::new_background_event(format!("{event:?}"))); + let text = format!("{event:?}"); + self.add_to_history(HistoryCell::new_background_event(text.clone())); + self.update_latest_log(text); } } } /// Update the live log preview while a task is running. pub(crate) fn update_latest_log(&mut self, line: String) { - // Forward only if we are currently showing the status indicator. self.bottom_pane.update_status_text(line); } @@ -515,6 +516,97 @@ impl ChatWidget<'_> { } } +impl ChatWidget<'_> { + fn begin_stream(&mut self, kind: StreamKind) { + if self.current_stream != Some(kind) { + self.current_stream = Some(kind); + self.stream_header_emitted = false; + // Clear any previous live content; we're starting a new stream. + self.live_builder = RowBuilder::new(self.live_builder.width()); + // Ensure the waiting status is visible (composer replaced). + self.bottom_pane + .update_status_text("waiting for model".to_string()); + } + } + + fn stream_push_and_maybe_commit(&mut self, delta: &str) { + self.live_builder.push_fragment(delta); + + // Commit overflow rows (small batches) while keeping the last N rows visible. + let drained = self + .live_builder + .drain_commit_ready(self.live_max_rows as usize); + if !drained.is_empty() { + let mut lines: Vec> = Vec::new(); + if !self.stream_header_emitted { + match self.current_stream { + Some(StreamKind::Reasoning) => { + lines.push(ratatui::text::Line::from("thinking".magenta().italic())); + } + Some(StreamKind::Answer) => { + lines.push(ratatui::text::Line::from("codex".magenta().bold())); + } + None => {} + } + self.stream_header_emitted = true; + } + for r in drained { + lines.push(ratatui::text::Line::from(r.text)); + } + self.app_event_tx.send(AppEvent::InsertHistory(lines)); + } + + // Update the live ring overlay lines (text-only, newest at bottom). + let rows = self + .live_builder + .display_rows() + .into_iter() + .map(|r| ratatui::text::Line::from(r.text)) + .collect::>(); + self.bottom_pane + .set_live_ring_rows(self.live_max_rows, rows); + } + + fn finalize_stream(&mut self, kind: StreamKind) { + if self.current_stream != Some(kind) { + // Nothing to do; either already finalized or not the active stream. + return; + } + // Flush any partial line as a full row, then drain all remaining rows. + self.live_builder.end_line(); + let remaining = self.live_builder.drain_rows(); + // TODO: Re-add markdown rendering for assistant answers and reasoning. + // When finalizing, pass the accumulated text through `markdown::append_markdown` + // to build styled `Line<'static>` entries instead of raw plain text lines. + if !remaining.is_empty() || !self.stream_header_emitted { + let mut lines: Vec> = Vec::new(); + if !self.stream_header_emitted { + match kind { + StreamKind::Reasoning => { + lines.push(ratatui::text::Line::from("thinking".magenta().italic())); + } + StreamKind::Answer => { + lines.push(ratatui::text::Line::from("codex".magenta().bold())); + } + } + self.stream_header_emitted = true; + } + for r in remaining { + lines.push(ratatui::text::Line::from(r.text)); + } + // Close the block with a blank line for readability. + lines.push(ratatui::text::Line::from("")); + self.app_event_tx.send(AppEvent::InsertHistory(lines)); + } + + // Clear the live overlay and reset state for the next stream. + self.live_builder = RowBuilder::new(self.live_builder.width()); + self.bottom_pane.clear_live_ring(); + self.current_stream = None; + self.stream_header_emitted = false; + } +} + impl WidgetRef for &ChatWidget<'_> { fn render_ref(&self, area: Rect, buf: &mut Buffer) { // In the hybrid inline viewport mode we only draw the interactive diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index c2aafdd522..17f0e683c0 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -1,5 +1,4 @@ use crate::exec_command::strip_bash_lc_and_escape; -use crate::markdown::append_markdown; use crate::text_block::TextBlock; use crate::text_formatting::format_and_truncate_tool_result; use base64::Engine; @@ -68,12 +67,7 @@ pub(crate) enum HistoryCell { /// Message from the user. UserPrompt { view: TextBlock }, - /// Message from the agent. - AgentMessage { view: TextBlock }, - - /// Reasoning event from the agent. - AgentReasoning { view: TextBlock }, - + // AgentMessage and AgentReasoning variants were unused and have been removed. /// An exec tool call that has not finished yet. ActiveExecCommand { view: TextBlock }, @@ -128,8 +122,6 @@ impl HistoryCell { match self { HistoryCell::WelcomeMessage { view } | HistoryCell::UserPrompt { view } - | HistoryCell::AgentMessage { view } - | HistoryCell::AgentReasoning { view } | HistoryCell::BackgroundEvent { view } | HistoryCell::GitDiffOutput { view } | HistoryCell::ErrorEvent { view } @@ -231,28 +223,6 @@ impl HistoryCell { } } - pub(crate) fn new_agent_message(config: &Config, message: String) -> Self { - let mut lines: Vec> = Vec::new(); - lines.push(Line::from("codex".magenta().bold())); - append_markdown(&message, &mut lines, config); - lines.push(Line::from("")); - - HistoryCell::AgentMessage { - view: TextBlock::new(lines), - } - } - - pub(crate) fn new_agent_reasoning(config: &Config, text: String) -> Self { - let mut lines: Vec> = Vec::new(); - lines.push(Line::from("thinking".magenta().italic())); - append_markdown(&text, &mut lines, config); - lines.push(Line::from("")); - - HistoryCell::AgentReasoning { - view: TextBlock::new(lines), - } - } - pub(crate) fn new_active_exec_command(command: Vec) -> Self { let command_escaped = strip_bash_lc_and_escape(&command); diff --git a/codex-rs/tui/src/insert_history.rs b/codex-rs/tui/src/insert_history.rs index 87d88b7f03..5c316637b1 100644 --- a/codex-rs/tui/src/insert_history.rs +++ b/codex-rs/tui/src/insert_history.rs @@ -14,7 +14,6 @@ use crossterm::style::SetBackgroundColor; use crossterm::style::SetColors; use crossterm::style::SetForegroundColor; use ratatui::layout::Size; -use ratatui::prelude::Backend; use ratatui::style::Color; use ratatui::style::Modifier; use ratatui::text::Line; @@ -22,6 +21,20 @@ use ratatui::text::Span; /// Insert `lines` above the viewport. pub(crate) fn insert_history_lines(terminal: &mut tui::Tui, lines: Vec) { + let mut out = std::io::stdout(); + insert_history_lines_to_writer(terminal, &mut out, lines); +} + +/// Like `insert_history_lines`, but writes ANSI to the provided writer. This +/// is intended for testing where a capture buffer is used instead of stdout. +pub fn insert_history_lines_to_writer( + terminal: &mut crate::custom_terminal::Terminal, + writer: &mut W, + lines: Vec, +) where + B: ratatui::backend::Backend, + W: Write, +{ let screen_size = terminal.backend().size().unwrap_or(Size::new(0, 0)); let cursor_pos = terminal.get_cursor_position().ok(); @@ -32,10 +45,22 @@ pub(crate) fn insert_history_lines(terminal: &mut tui::Tui, lines: Vec) { // If the viewport is not at the bottom of the screen, scroll it down to make room. // Don't scroll it past the bottom of the screen. let scroll_amount = wrapped_lines.min(screen_size.height - area.bottom()); - terminal - .backend_mut() - .scroll_region_down(area.top()..screen_size.height, scroll_amount) - .ok(); + + // Emit ANSI to scroll the lower region (from the top of the viewport to the bottom + // of the screen) downward by `scroll_amount` lines. We do this by: + // 1) Limiting the scroll region to [area.top()+1 .. screen_height] (1-based bounds) + // 2) Placing the cursor at the top margin of that region + // 3) Emitting Reverse Index (RI, ESC M) `scroll_amount` times + // 4) Resetting the scroll region back to full screen + let top_1based = area.top() + 1; // Convert 0-based row to 1-based for DECSTBM + queue!(writer, SetScrollRegion(top_1based..screen_size.height)).ok(); + queue!(writer, MoveTo(0, area.top())).ok(); + for _ in 0..scroll_amount { + // Reverse Index (RI): ESC M + queue!(writer, Print("\x1bM")).ok(); + } + queue!(writer, ResetScrollRegion).ok(); + let cursor_top = area.top().saturating_sub(1); area.y += scroll_amount; terminal.set_viewport_area(area); @@ -59,23 +84,23 @@ pub(crate) fn insert_history_lines(terminal: &mut tui::Tui, lines: Vec) { // ││ ││ // │╰────────────────────────────╯│ // └──────────────────────────────┘ - queue!(std::io::stdout(), SetScrollRegion(1..area.top())).ok(); + queue!(writer, SetScrollRegion(1..area.top())).ok(); // NB: we are using MoveTo instead of set_cursor_position here to avoid messing with the // terminal's last_known_cursor_position, which hopefully will still be accurate after we // fetch/restore the cursor position. insert_history_lines should be cursor-position-neutral :) - queue!(std::io::stdout(), MoveTo(0, cursor_top)).ok(); + queue!(writer, MoveTo(0, cursor_top)).ok(); for line in lines { - queue!(std::io::stdout(), Print("\r\n")).ok(); - write_spans(&mut std::io::stdout(), line.iter()).ok(); + queue!(writer, Print("\r\n")).ok(); + write_spans(writer, line.iter()).ok(); } - queue!(std::io::stdout(), ResetScrollRegion).ok(); + queue!(writer, ResetScrollRegion).ok(); // Restore the cursor position to where it was before we started. if let Some(cursor_pos) = cursor_pos { - queue!(std::io::stdout(), MoveTo(cursor_pos.x, cursor_pos.y)).ok(); + queue!(writer, MoveTo(cursor_pos.x, cursor_pos.y)).ok(); } } @@ -88,19 +113,25 @@ fn wrapped_line_count(lines: &[Line], width: u16) -> u16 { } fn line_height(line: &Line, width: u16) -> u16 { - use unicode_width::UnicodeWidthStr; - // get the total display width of the line, accounting for double-width chars - let total_width = line + // Use the same visible-width slicing semantics as the live row builder so + // our pre-scroll estimation matches how rows will actually wrap. + let w = width.max(1) as usize; + let mut rows = 0u16; + let mut remaining = line .spans .iter() - .map(|span| span.content.width()) - .sum::(); - // divide by width to get the number of lines, rounding up - if width == 0 { - 1 - } else { - (total_width as u16).div_ceil(width).max(1) + .map(|s| s.content.as_ref()) + .collect::>() + .join(""); + while !remaining.is_empty() { + let (_prefix, suffix, taken) = crate::live_wrap::take_prefix_by_width(&remaining, w); + rows = rows.saturating_add(1); + if taken >= remaining.len() { + break; + } + remaining = suffix.to_string(); } + rows.max(1) } #[derive(Debug, Clone, PartialEq, Eq)] @@ -283,4 +314,12 @@ mod tests { String::from_utf8(expected).unwrap() ); } + + #[test] + fn line_height_counts_double_width_emoji() { + let line = Line::from("😀😀😀"); // each emoji ~ width 2 + assert_eq!(line_height(&line, 4), 2); + assert_eq!(line_height(&line, 2), 3); + assert_eq!(line_height(&line, 6), 1); + } } diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 0ec9be6153..c619ce8ff0 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -25,13 +25,14 @@ mod bottom_pane; mod chatwidget; mod citation_regex; mod cli; -mod custom_terminal; +pub mod custom_terminal; mod exec_command; mod file_search; mod get_git_diff; mod git_warning_screen; mod history_cell; -mod insert_history; +pub mod insert_history; +pub mod live_wrap; mod log_layer; mod markdown; mod slash_command; diff --git a/codex-rs/tui/src/live_wrap.rs b/codex-rs/tui/src/live_wrap.rs new file mode 100644 index 0000000000..e78710dc6c --- /dev/null +++ b/codex-rs/tui/src/live_wrap.rs @@ -0,0 +1,290 @@ +use unicode_width::UnicodeWidthChar; +use unicode_width::UnicodeWidthStr; + +/// A single visual row produced by RowBuilder. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Row { + pub text: String, + /// True if this row ends with an explicit line break (as opposed to a hard wrap). + pub explicit_break: bool, +} + +impl Row { + pub fn width(&self) -> usize { + self.text.width() + } +} + +/// Incrementally wraps input text into visual rows of at most `width` cells. +/// +/// Step 1: plain-text only. ANSI-carry and styled spans will be added later. +pub struct RowBuilder { + target_width: usize, + /// Buffer for the current logical line (until a '\n' is seen). + current_line: String, + /// Output rows built so far for the current logical line and previous ones. + rows: Vec, +} + +impl RowBuilder { + pub fn new(target_width: usize) -> Self { + Self { + target_width: target_width.max(1), + current_line: String::new(), + rows: Vec::new(), + } + } + + pub fn width(&self) -> usize { + self.target_width + } + + pub fn set_width(&mut self, width: usize) { + self.target_width = width.max(1); + // Rewrap everything we have (simple approach for Step 1). + let mut all = String::new(); + for row in self.rows.drain(..) { + all.push_str(&row.text); + if row.explicit_break { + all.push('\n'); + } + } + all.push_str(&self.current_line); + self.current_line.clear(); + self.push_fragment(&all); + } + + /// Push an input fragment. May contain newlines. + pub fn push_fragment(&mut self, fragment: &str) { + if fragment.is_empty() { + return; + } + let mut start = 0usize; + for (i, ch) in fragment.char_indices() { + if ch == '\n' { + // Flush anything pending before the newline. + if start < i { + self.current_line.push_str(&fragment[start..i]); + } + self.flush_current_line(true); + start = i + ch.len_utf8(); + } + } + if start < fragment.len() { + self.current_line.push_str(&fragment[start..]); + self.wrap_current_line(); + } + } + + /// Mark the end of the current logical line (equivalent to pushing a '\n'). + pub fn end_line(&mut self) { + self.flush_current_line(true); + } + + /// Drain and return all produced rows. + pub fn drain_rows(&mut self) -> Vec { + std::mem::take(&mut self.rows) + } + + /// Return a snapshot of produced rows (non-draining). + pub fn rows(&self) -> &[Row] { + &self.rows + } + + /// Rows suitable for display, including the current partial line if any. + pub fn display_rows(&self) -> Vec { + let mut out = self.rows.clone(); + if !self.current_line.is_empty() { + out.push(Row { + text: self.current_line.clone(), + explicit_break: false, + }); + } + out + } + + /// Drain the oldest rows that exceed `max_keep` display rows (including the + /// current partial line, if any). Returns the drained rows in order. + pub fn drain_commit_ready(&mut self, max_keep: usize) -> Vec { + let display_count = self.rows.len() + if self.current_line.is_empty() { 0 } else { 1 }; + if display_count <= max_keep { + return Vec::new(); + } + let to_commit = display_count - max_keep; + let commit_count = to_commit.min(self.rows.len()); + let mut drained = Vec::with_capacity(commit_count); + for _ in 0..commit_count { + drained.push(self.rows.remove(0)); + } + drained + } + + fn flush_current_line(&mut self, explicit_break: bool) { + // Wrap any remaining content in the current line and then finalize with explicit_break. + self.wrap_current_line(); + // If the current line ended exactly on a width boundary and is non-empty, represent + // the explicit break as an empty explicit row so that fragmentation invariance holds. + if explicit_break { + if self.current_line.is_empty() { + // We ended on a boundary previously; add an empty explicit row. + self.rows.push(Row { + text: String::new(), + explicit_break: true, + }); + } else { + // There is leftover content that did not wrap yet; push it now with the explicit flag. + let mut s = String::new(); + std::mem::swap(&mut s, &mut self.current_line); + self.rows.push(Row { + text: s, + explicit_break: true, + }); + } + } + // Reset current line buffer for next logical line. + self.current_line.clear(); + } + + fn wrap_current_line(&mut self) { + // While the current_line exceeds width, cut a prefix. + loop { + if self.current_line.is_empty() { + break; + } + let (prefix, suffix, taken) = + take_prefix_by_width(&self.current_line, self.target_width); + if taken == 0 { + // Avoid infinite loop on pathological inputs; take one scalar and continue. + if let Some((i, ch)) = self.current_line.char_indices().next() { + let len = i + ch.len_utf8(); + let p = self.current_line[..len].to_string(); + self.rows.push(Row { + text: p, + explicit_break: false, + }); + self.current_line = self.current_line[len..].to_string(); + continue; + } + break; + } + if suffix.is_empty() { + // Fits entirely; keep in buffer (do not push yet) so we can append more later. + break; + } else { + // Emit wrapped prefix as a non-explicit row and continue with the remainder. + self.rows.push(Row { + text: prefix, + explicit_break: false, + }); + self.current_line = suffix.to_string(); + } + } + } +} + +/// Take a prefix of `text` whose visible width is at most `max_cols`. +/// Returns (prefix, suffix, prefix_width). +pub fn take_prefix_by_width(text: &str, max_cols: usize) -> (String, &str, usize) { + if max_cols == 0 || text.is_empty() { + return (String::new(), text, 0); + } + let mut cols = 0usize; + let mut end_idx = 0usize; + for (i, ch) in text.char_indices() { + let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0); + if cols.saturating_add(ch_width) > max_cols { + break; + } + cols += ch_width; + end_idx = i + ch.len_utf8(); + if cols == max_cols { + break; + } + } + let prefix = text[..end_idx].to_string(); + let suffix = &text[end_idx..]; + (prefix, suffix, cols) +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn rows_do_not_exceed_width_ascii() { + let mut rb = RowBuilder::new(10); + rb.push_fragment("hello whirl this is a test"); + let rows = rb.rows().to_vec(); + assert_eq!( + rows, + vec![ + Row { + text: "hello whir".to_string(), + explicit_break: false + }, + Row { + text: "l this is ".to_string(), + explicit_break: false + } + ] + ); + } + + #[test] + fn rows_do_not_exceed_width_emoji_cjk() { + // 😀 is width 2; 你/好 are width 2. + let mut rb = RowBuilder::new(6); + rb.push_fragment("😀😀 你好"); + let rows = rb.rows().to_vec(); + // At width 6, we expect the first row to fit exactly two emojis and a space + // (2 + 2 + 1 = 5) plus one more column for the first CJK char (2 would overflow), + // so only the two emojis and the space fit; the rest remains buffered. + assert_eq!( + rows, + vec![Row { + text: "😀😀 ".to_string(), + explicit_break: false + }] + ); + } + + #[test] + fn fragmentation_invariance_long_token() { + let s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; // 26 chars + let mut rb_all = RowBuilder::new(7); + rb_all.push_fragment(s); + let all_rows = rb_all.rows().to_vec(); + + let mut rb_chunks = RowBuilder::new(7); + for i in (0..s.len()).step_by(3) { + let end = (i + 3).min(s.len()); + rb_chunks.push_fragment(&s[i..end]); + } + let chunk_rows = rb_chunks.rows().to_vec(); + + assert_eq!(all_rows, chunk_rows); + } + + #[test] + fn newline_splits_rows() { + let mut rb = RowBuilder::new(10); + rb.push_fragment("hello\nworld"); + let rows = rb.display_rows(); + assert!(rows.iter().any(|r| r.explicit_break)); + assert_eq!(rows[0].text, "hello"); + // Second row should begin with 'world' + assert!(rows.iter().any(|r| r.text.starts_with("world"))); + } + + #[test] + fn rewrap_on_width_change() { + let mut rb = RowBuilder::new(10); + rb.push_fragment("abcdefghijK"); + assert!(!rb.rows().is_empty()); + rb.set_width(5); + for r in rb.rows() { + assert!(r.width() <= 5); + } + } +} diff --git a/codex-rs/tui/src/markdown.rs b/codex-rs/tui/src/markdown.rs index ab20138298..910a6869ec 100644 --- a/codex-rs/tui/src/markdown.rs +++ b/codex-rs/tui/src/markdown.rs @@ -1,3 +1,4 @@ +use crate::citation_regex::CITATION_REGEX; use codex_core::config::Config; use codex_core::config_types::UriBasedFileOpener; use ratatui::text::Line; @@ -5,8 +6,7 @@ use ratatui::text::Span; use std::borrow::Cow; use std::path::Path; -use crate::citation_regex::CITATION_REGEX; - +#[allow(dead_code)] pub(crate) fn append_markdown( markdown_source: &str, lines: &mut Vec>, @@ -15,6 +15,7 @@ pub(crate) fn append_markdown( append_markdown_with_opener_and_cwd(markdown_source, lines, config.file_opener, &config.cwd); } +#[allow(dead_code)] fn append_markdown_with_opener_and_cwd( markdown_source: &str, lines: &mut Vec>, @@ -60,6 +61,7 @@ fn append_markdown_with_opener_and_cwd( /// ```text /// ://file: /// ``` +#[allow(dead_code)] fn rewrite_file_citations<'a>( src: &'a str, file_opener: UriBasedFileOpener, diff --git a/codex-rs/tui/src/status_indicator_widget.rs b/codex-rs/tui/src/status_indicator_widget.rs index aa18ac6fa5..fad7e41a39 100644 --- a/codex-rs/tui/src/status_indicator_widget.rs +++ b/codex-rs/tui/src/status_indicator_widget.rs @@ -9,24 +9,22 @@ use std::thread; use std::time::Duration; use ratatui::buffer::Buffer; -use ratatui::layout::Alignment; use ratatui::layout::Rect; use ratatui::style::Color; use ratatui::style::Modifier; use ratatui::style::Style; -use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::text::Span; -use ratatui::widgets::Block; -use ratatui::widgets::BorderType; -use ratatui::widgets::Borders; -use ratatui::widgets::Padding; use ratatui::widgets::Paragraph; use ratatui::widgets::WidgetRef; +use unicode_width::UnicodeWidthStr; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; +// We render the live text using markdown so it visually matches the history +// cells. Before rendering we strip any ANSI escape sequences to avoid writing +// raw control bytes into the back buffer. use codex_ansi_escape::ansi_escape_line; pub(crate) struct StatusIndicatorWidget { @@ -34,6 +32,14 @@ pub(crate) struct StatusIndicatorWidget { /// time). text: String, + /// Animation state: reveal target `text` progressively like a typewriter. + /// We compute the currently visible prefix length based on the current + /// frame index and a constant typing speed. The `base_frame` and + /// `reveal_len_at_base` form the anchor from which we advance. + last_target_len: usize, + base_frame: usize, + reveal_len_at_base: usize, + frame_idx: Arc, running: Arc, // Keep one sender alive to prevent the channel from closing while the @@ -66,9 +72,13 @@ impl StatusIndicatorWidget { } Self { - text: String::from("waiting for logs…"), + text: String::from("waiting for model"), + last_target_len: 0, + base_frame: 0, + reveal_len_at_base: 0, frame_idx, running, + _app_event_tx: app_event_tx, } } @@ -79,7 +89,67 @@ impl StatusIndicatorWidget { /// Update the line that is displayed in the widget. pub(crate) fn update_text(&mut self, text: String) { - self.text = text.replace(['\n', '\r'], " "); + // If the text hasn't changed, don't reset the baseline; let the + // animation continue advancing naturally. + if text == self.text { + return; + } + // Update the target text, preserving newlines so wrapping matches history cells. + // Strip ANSI escapes for the character count so the typewriter animation speed is stable. + let stripped = { + let line = ansi_escape_line(&text); + line.spans + .iter() + .map(|s| s.content.as_ref()) + .collect::>() + .join("") + }; + let new_len = stripped.chars().count(); + + // Compute how many characters are currently revealed so we can carry + // this forward as the new baseline when target text changes. + let current_frame = self.frame_idx.load(std::sync::atomic::Ordering::Relaxed); + let shown_now = self.current_shown_len(current_frame); + + self.text = text; + self.last_target_len = new_len; + self.base_frame = current_frame; + self.reveal_len_at_base = shown_now.min(new_len); + } + + /// Reset the animation and start revealing `text` from the beginning. + #[cfg(test)] + pub(crate) fn restart_with_text(&mut self, text: String) { + let sanitized = text.replace(['\n', '\r'], " "); + let stripped = { + let line = ansi_escape_line(&sanitized); + line.spans + .iter() + .map(|s| s.content.as_ref()) + .collect::>() + .join("") + }; + + let new_len = stripped.chars().count(); + let current_frame = self.frame_idx.load(std::sync::atomic::Ordering::Relaxed); + + self.text = sanitized; + self.last_target_len = new_len; + self.base_frame = current_frame; + // Start from zero revealed characters for a fresh typewriter cycle. + self.reveal_len_at_base = 0; + } + + /// Calculate how many characters should currently be visible given the + /// animation baseline and frame counter. + fn current_shown_len(&self, current_frame: usize) -> usize { + // Increase typewriter speed (~5x): reveal more characters per frame. + const TYPING_CHARS_PER_FRAME: usize = 7; + let frames = current_frame.saturating_sub(self.base_frame); + let advanced = self + .reveal_len_at_base + .saturating_add(frames.saturating_mul(TYPING_CHARS_PER_FRAME)); + advanced.min(self.last_target_len) } } @@ -92,26 +162,22 @@ impl Drop for StatusIndicatorWidget { impl WidgetRef for StatusIndicatorWidget { fn render_ref(&self, area: Rect, buf: &mut Buffer) { - let widget_style = Style::default(); - let block = Block::default() - .padding(Padding::new(1, 0, 0, 0)) - .borders(Borders::LEFT) - .border_type(BorderType::QuadrantOutside) - .border_style(widget_style.dim()); + // Ensure minimal height + if area.height == 0 || area.width == 0 { + return; + } + + // Build animated gradient header for the word "Working". let idx = self.frame_idx.load(std::sync::atomic::Ordering::Relaxed); let header_text = "Working"; let header_chars: Vec = header_text.chars().collect(); - let padding = 4usize; // virtual padding around the word for smoother loop let period = header_chars.len() + padding * 2; let pos = idx % period; - let has_true_color = supports_color::on_cached(supports_color::Stream::Stdout) .map(|level| level.has_16m) .unwrap_or(false); - - // Width of the bright band (in characters). - let band_half_width = 2.0; + let band_half_width = 2.0; // width of the bright band in characters let mut header_spans: Vec> = Vec::new(); for (i, ch) in header_chars.iter().enumerate() { @@ -133,64 +199,46 @@ impl WidgetRef for StatusIndicatorWidget { .fg(Color::Rgb(level, level, level)) .add_modifier(Modifier::BOLD) } else { - // Bold makes dark gray and gray look the same, so don't use it - // when true color is not supported. + // Bold makes dark gray and gray look the same, so don't use it when true color is not supported. Style::default().fg(color_for_level(level)) }; header_spans.push(Span::styled(ch.to_string(), style)); } - header_spans.push(Span::styled( + // Plain rendering: no borders or padding so the live cell is visually indistinguishable from terminal scrollback. + let inner_width = area.width as usize; + + // Compose a single status line like: "▌ Working [•] waiting for model" + let mut spans: Vec> = Vec::new(); + spans.push(Span::styled("▌ ", Style::default().fg(Color::Cyan))); + // Gradient header + spans.extend(header_spans); + // Space after header + spans.push(Span::styled( " ", Style::default() .fg(Color::White) .add_modifier(Modifier::BOLD), )); - // Ensure we do not overflow width. - let inner_width = block.inner(area).width as usize; - - // Sanitize and colour‑strip the potentially colourful log text. This - // ensures that **no** raw ANSI escape sequences leak into the - // back‑buffer which would otherwise cause cursor jumps or stray - // artefacts when the terminal is resized. - let line = ansi_escape_line(&self.text); - let mut sanitized_tail: String = line - .spans - .iter() - .map(|s| s.content.as_ref()) - .collect::>() - .join(""); - - // Truncate *after* stripping escape codes so width calculation is - // accurate. See UTF‑8 boundary comments above. - let header_len: usize = header_spans.iter().map(|s| s.content.len()).sum(); - - if header_len + sanitized_tail.len() > inner_width { - let available_bytes = inner_width.saturating_sub(header_len); - - if sanitized_tail.is_char_boundary(available_bytes) { - sanitized_tail.truncate(available_bytes); + // Truncate spans to fit the width. + let mut acc: Vec> = Vec::new(); + let mut used = 0usize; + for s in spans { + let w = s.content.width(); + if used + w <= inner_width { + acc.push(s); + used += w; } else { - let mut idx = available_bytes; - while idx < sanitized_tail.len() && !sanitized_tail.is_char_boundary(idx) { - idx += 1; - } - sanitized_tail.truncate(idx); + break; } } + let lines = vec![Line::from(acc)]; - let mut spans = header_spans; + // No-op once full text is revealed; the app no longer reacts to a completion event. - // Re‑apply the DIM modifier so the tail appears visually subdued - // irrespective of the colour information preserved by - // `ansi_escape_line`. - spans.push(Span::styled(sanitized_tail, Style::default().dim())); - - let paragraph = Paragraph::new(Line::from(spans)) - .block(block) - .alignment(Alignment::Left); + let paragraph = Paragraph::new(lines); paragraph.render_ref(area, buf); } } @@ -204,3 +252,50 @@ fn color_for_level(level: u8) -> Color { Color::White } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::app_event::AppEvent; + use crate::app_event_sender::AppEventSender; + use std::sync::mpsc::channel; + + #[test] + fn renders_without_left_border_or_padding() { + let (tx_raw, _rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let mut w = StatusIndicatorWidget::new(tx); + w.restart_with_text("Hello".to_string()); + + let area = ratatui::layout::Rect::new(0, 0, 30, 1); + // Allow a short delay so the typewriter reveals the first character. + std::thread::sleep(std::time::Duration::from_millis(120)); + let mut buf = ratatui::buffer::Buffer::empty(area); + w.render_ref(area, &mut buf); + + // Leftmost column has the left bar + let ch0 = buf[(0, 0)].symbol().chars().next().unwrap_or(' '); + assert_eq!(ch0, '▌', "expected left bar at col 0: {ch0:?}"); + } + + #[test] + fn working_header_is_present_on_last_line() { + let (tx_raw, _rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let mut w = StatusIndicatorWidget::new(tx); + w.restart_with_text("Hi".to_string()); + // Ensure some frames elapse so we get a stable state. + std::thread::sleep(std::time::Duration::from_millis(120)); + + let area = ratatui::layout::Rect::new(0, 0, 30, 1); + let mut buf = ratatui::buffer::Buffer::empty(area); + w.render_ref(area, &mut buf); + + // Single line; it should contain the animated "Working" header. + let mut row = String::new(); + for x in 0..area.width { + row.push(buf[(x, 0)].symbol().chars().next().unwrap_or(' ')); + } + assert!(row.contains("Working"), "expected Working header: {row:?}"); + } +} diff --git a/codex-rs/tui/tests/vt100_history.rs b/codex-rs/tui/tests/vt100_history.rs new file mode 100644 index 0000000000..11ee044041 --- /dev/null +++ b/codex-rs/tui/tests/vt100_history.rs @@ -0,0 +1,214 @@ +#![cfg(feature = "vt100-tests")] +#![expect(clippy::expect_used)] + +use ratatui::backend::TestBackend; +use ratatui::layout::Rect; +use ratatui::style::Color; +use ratatui::style::Style; +use ratatui::text::Line; +use ratatui::text::Span; + +// Small helper macro to assert a collection contains an item with a clearer +// failure message. +macro_rules! assert_contains { + ($collection:expr, $item:expr $(,)?) => { + assert!( + $collection.contains(&$item), + "Expected {:?} to contain {:?}", + $collection, + $item + ); + }; + ($collection:expr, $item:expr, $($arg:tt)+) => { + assert!($collection.contains(&$item), $($arg)+); + }; +} + +struct TestScenario { + width: u16, + height: u16, + term: codex_tui::custom_terminal::Terminal, +} + +impl TestScenario { + fn new(width: u16, height: u16, viewport: Rect) -> Self { + let backend = TestBackend::new(width, height); + let mut term = codex_tui::custom_terminal::Terminal::with_options(backend) + .expect("failed to construct terminal"); + term.set_viewport_area(viewport); + Self { + width, + height, + term, + } + } + + fn run_insert(&mut self, lines: Vec>) -> Vec { + let mut buf: Vec = Vec::new(); + codex_tui::insert_history::insert_history_lines_to_writer(&mut self.term, &mut buf, lines); + buf + } + + fn screen_rows_from_bytes(&self, bytes: &[u8]) -> Vec { + let mut parser = vt100::Parser::new(self.height, self.width, 0); + parser.process(bytes); + let screen = parser.screen(); + + let mut rows: Vec = Vec::with_capacity(self.height as usize); + for row in 0..self.height { + let mut s = String::with_capacity(self.width as usize); + for col in 0..self.width { + if let Some(cell) = screen.cell(row, col) { + if let Some(ch) = cell.contents().chars().next() { + s.push(ch); + } else { + s.push(' '); + } + } else { + s.push(' '); + } + } + rows.push(s.trim_end().to_string()); + } + rows + } +} + +#[test] +fn hist_001_basic_insertion_no_wrap() { + // Screen of 20x6; viewport is the last row (height=1 at y=5) + let area = Rect::new(0, 5, 20, 1); + let mut scenario = TestScenario::new(20, 6, area); + + let lines = vec![Line::from("first"), Line::from("second")]; + let buf = scenario.run_insert(lines); + let rows = scenario.screen_rows_from_bytes(&buf); + assert_contains!(rows, String::from("first")); + assert_contains!(rows, String::from("second")); + let first_idx = rows + .iter() + .position(|r| r == "first") + .expect("expected 'first' row to be present"); + let second_idx = rows + .iter() + .position(|r| r == "second") + .expect("expected 'second' row to be present"); + assert_eq!(second_idx, first_idx + 1, "rows should be adjacent"); +} + +#[test] +fn hist_002_long_token_wraps() { + let area = Rect::new(0, 5, 20, 1); + let mut scenario = TestScenario::new(20, 6, area); + + let long = "A".repeat(45); // > 2 lines at width 20 + let lines = vec![Line::from(long.clone())]; + let buf = scenario.run_insert(lines); + let mut parser = vt100::Parser::new(6, 20, 0); + parser.process(&buf); + let screen = parser.screen(); + + // Count total A's on the screen + let mut count_a = 0usize; + for row in 0..6 { + for col in 0..20 { + if let Some(cell) = screen.cell(row, col) { + if let Some(ch) = cell.contents().chars().next() { + if ch == 'A' { + count_a += 1; + } + } + } + } + } + + assert_eq!( + count_a, + long.len(), + "wrapped content did not preserve all characters" + ); +} + +#[test] +fn hist_003_emoji_and_cjk() { + let area = Rect::new(0, 5, 20, 1); + let mut scenario = TestScenario::new(20, 6, area); + + let text = String::from("😀😀😀😀😀 你好世界"); + let lines = vec![Line::from(text.clone())]; + let buf = scenario.run_insert(lines); + let rows = scenario.screen_rows_from_bytes(&buf); + let reconstructed: String = rows.join("").chars().filter(|c| *c != ' ').collect(); + for ch in text.chars().filter(|c| !c.is_whitespace()) { + assert!( + reconstructed.contains(ch), + "missing character {ch:?} in reconstructed screen" + ); + } +} + +#[test] +fn hist_004_mixed_ansi_spans() { + let area = Rect::new(0, 5, 20, 1); + let mut scenario = TestScenario::new(20, 6, area); + + let line = Line::from(vec![ + Span::styled("red", Style::default().fg(Color::Red)), + Span::raw("+plain"), + ]); + let buf = scenario.run_insert(vec![line]); + let rows = scenario.screen_rows_from_bytes(&buf); + assert_contains!(rows, String::from("red+plain")); +} + +#[test] +fn hist_006_cursor_restoration() { + let area = Rect::new(0, 5, 20, 1); + let mut scenario = TestScenario::new(20, 6, area); + + let lines = vec![Line::from("x")]; + let buf = scenario.run_insert(lines); + let s = String::from_utf8_lossy(&buf); + // CUP to 1;1 (ANSI: ESC[1;1H) + assert!( + s.contains("\u{1b}[1;1H"), + "expected final CUP to 1;1 in output, got: {s:?}" + ); + // Reset scroll region + assert!( + s.contains("\u{1b}[r"), + "expected reset scroll region in output, got: {s:?}" + ); +} + +#[test] +fn hist_005_pre_scroll_region_down() { + // Viewport not at bottom: y=3 (0-based), height=1 + let area = Rect::new(0, 3, 20, 1); + let mut scenario = TestScenario::new(20, 6, area); + + let lines = vec![Line::from("first"), Line::from("second")]; + let buf = scenario.run_insert(lines); + let s = String::from_utf8_lossy(&buf); + // Expect we limited scroll region to [top+1 .. screen_height] => [4 .. 6] (1-based) + assert!( + s.contains("\u{1b}[4;6r"), + "expected pre-scroll SetScrollRegion 4..6, got: {s:?}" + ); + // Expect we moved cursor to top of that region: row 3 (0-based) => CUP 4;1H + assert!( + s.contains("\u{1b}[4;1H"), + "expected cursor at top of pre-scroll region, got: {s:?}" + ); + // Expect at least two Reverse Index commands (ESC M) for two inserted lines + let ri_count = s.matches("\u{1b}M").count(); + assert!( + ri_count >= 1, + "expected at least one RI (ESC M), got: {s:?}" + ); + // After pre-scroll, we set insertion scroll region to [1 .. new_top] => [1 .. 5] + assert!( + s.contains("\u{1b}[1;5r"), + "expected insertion SetScrollRegion 1..5, got: {s:?}" + ); +} diff --git a/codex-rs/tui/tests/vt100_live_commit.rs b/codex-rs/tui/tests/vt100_live_commit.rs new file mode 100644 index 0000000000..c0cfb3211a --- /dev/null +++ b/codex-rs/tui/tests/vt100_live_commit.rs @@ -0,0 +1,101 @@ +#![cfg(feature = "vt100-tests")] + +use ratatui::backend::TestBackend; +use ratatui::layout::Rect; +use ratatui::text::Line; + +#[test] +fn live_001_commit_on_overflow() { + let backend = TestBackend::new(20, 6); + let mut term = match codex_tui::custom_terminal::Terminal::with_options(backend) { + Ok(t) => t, + Err(e) => panic!("failed to construct terminal: {e}"), + }; + let area = Rect::new(0, 5, 20, 1); + term.set_viewport_area(area); + + // Build 5 explicit rows at width 20. + let mut rb = codex_tui::live_wrap::RowBuilder::new(20); + rb.push_fragment("one\n"); + rb.push_fragment("two\n"); + rb.push_fragment("three\n"); + rb.push_fragment("four\n"); + rb.push_fragment("five\n"); + + // Keep the last 3 in the live ring; commit the first 2. + let commit_rows = rb.drain_commit_ready(3); + let lines: Vec> = commit_rows + .into_iter() + .map(|r| Line::from(r.text)) + .collect(); + + let mut buf: Vec = Vec::new(); + codex_tui::insert_history::insert_history_lines_to_writer(&mut term, &mut buf, lines); + + let mut parser = vt100::Parser::new(6, 20, 0); + parser.process(&buf); + let screen = parser.screen(); + + // The words "one" and "two" should appear above the viewport. + let mut joined = String::new(); + for row in 0..6 { + for col in 0..20 { + if let Some(cell) = screen.cell(row, col) { + if let Some(ch) = cell.contents().chars().next() { + joined.push(ch); + } else { + joined.push(' '); + } + } + } + joined.push('\n'); + } + assert!( + joined.contains("one"), + "expected committed 'one' to be visible\n{joined}" + ); + assert!( + joined.contains("two"), + "expected committed 'two' to be visible\n{joined}" + ); + // The last three (three,four,five) remain in the live ring, not committed here. +} + +#[test] +fn live_002_pre_scroll_and_commit() { + let backend = TestBackend::new(20, 6); + let mut term = match codex_tui::custom_terminal::Terminal::with_options(backend) { + Ok(t) => t, + Err(e) => panic!("failed to construct terminal: {e}"), + }; + // Viewport not at bottom: y=3 + let area = Rect::new(0, 3, 20, 1); + term.set_viewport_area(area); + + let mut rb = codex_tui::live_wrap::RowBuilder::new(20); + rb.push_fragment("alpha\n"); + rb.push_fragment("beta\n"); + rb.push_fragment("gamma\n"); + rb.push_fragment("delta\n"); + + // Keep 3, commit 1. + let commit_rows = rb.drain_commit_ready(3); + let lines: Vec> = commit_rows + .into_iter() + .map(|r| Line::from(r.text)) + .collect(); + + let mut buf: Vec = Vec::new(); + codex_tui::insert_history::insert_history_lines_to_writer(&mut term, &mut buf, lines); + let s = String::from_utf8_lossy(&buf); + + // Expect a SetScrollRegion to [area.top()+1 .. screen_height] and a cursor move to top of that region. + assert!( + s.contains("\u{1b}[4;6r"), + "expected pre-scroll region 4..6, got: {s:?}" + ); + assert!( + s.contains("\u{1b}[4;1H"), + "expected cursor CUP 4;1H, got: {s:?}" + ); +} From fcdb1c4b4da1fd774309abedeb257b1b060dc65d Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 4 Aug 2025 21:57:55 -0700 Subject: [PATCH 0015/1309] fix: disable reorderArrays in tamasfe.even-better-toml (#1837) The existing setting kept destroying my `~/.codex/config.toml` for the reasons mentioned in the comment. --- .vscode/settings.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 1712f5989b..aadeca0867 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,8 @@ "editor.defaultFormatter": "tamasfe.even-better-toml", "editor.formatOnSave": true, }, - "evenBetterToml.formatter.reorderArrays": true, + // Array order for options in ~/.codex/config.toml such as `notify` and the + // `args` for an MCP server is significant, so we disable reordering. + "evenBetterToml.formatter.reorderArrays": false, "evenBetterToml.formatter.reorderKeys": true, } From 136b3ee5bf23982622e554e507eddae5287b5ffe Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 4 Aug 2025 23:50:03 -0700 Subject: [PATCH 0016/1309] chore: introduce ModelFamily abstraction (#1838) To date, we have a number of hardcoded OpenAI model slug checks spread throughout the codebase, which makes it hard to audit the various special cases for each model. To mitigate this issue, this PR introduces the idea of a `ModelFamily` that has fields to represent the existing special cases, such as `supports_reasoning_summaries` and `uses_local_shell_tool`. There is a `find_family_for_model()` function that maps the raw model slug to a `ModelFamily`. This function hardcodes all the knowledge about the special attributes for each model. This PR then replaces the hardcoded model name checks with checks against a `ModelFamily`. Note `ModelFamily` is now available as `Config::model_family`. We should ultimately remove `Config::model` in favor of `Config::model_family::slug`. --- codex-rs/core/src/chat_completions.rs | 10 +-- codex-rs/core/src/client.rs | 12 ++-- codex-rs/core/src/client_common.rs | 38 +++-------- codex-rs/core/src/config.rs | 33 +++++---- codex-rs/core/src/lib.rs | 2 +- codex-rs/core/src/model_family.rs | 93 ++++++++++++++++++++++++++ codex-rs/core/src/openai_model_info.rs | 6 +- codex-rs/core/src/openai_tools.rs | 36 +++++----- codex-rs/exec/src/event_processor.rs | 3 +- codex-rs/tui/src/history_cell.rs | 3 +- 10 files changed, 161 insertions(+), 75 deletions(-) create mode 100644 codex-rs/core/src/model_family.rs diff --git a/codex-rs/core/src/chat_completions.rs b/codex-rs/core/src/chat_completions.rs index e1804b191e..6aeccc5dfb 100644 --- a/codex-rs/core/src/chat_completions.rs +++ b/codex-rs/core/src/chat_completions.rs @@ -21,6 +21,7 @@ use crate::client_common::ResponseEvent; use crate::client_common::ResponseStream; use crate::error::CodexErr; use crate::error::Result; +use crate::model_family::ModelFamily; use crate::models::ContentItem; use crate::models::ResponseItem; use crate::openai_tools::create_tools_json_for_chat_completions_api; @@ -29,7 +30,7 @@ use crate::util::backoff; /// Implementation for the classic Chat Completions API. pub(crate) async fn stream_chat_completions( prompt: &Prompt, - model: &str, + model_family: &ModelFamily, include_plan_tool: bool, client: &reqwest::Client, provider: &ModelProviderInfo, @@ -37,7 +38,7 @@ pub(crate) async fn stream_chat_completions( // Build messages array let mut messages = Vec::::new(); - let full_instructions = prompt.get_full_instructions(model); + let full_instructions = prompt.get_full_instructions(model_family); messages.push(json!({"role": "system", "content": full_instructions})); if let Some(instr) = &prompt.get_formatted_user_instructions() { @@ -110,9 +111,10 @@ pub(crate) async fn stream_chat_completions( } } - let tools_json = create_tools_json_for_chat_completions_api(prompt, model, include_plan_tool)?; + let tools_json = + create_tools_json_for_chat_completions_api(prompt, model_family, include_plan_tool)?; let payload = json!({ - "model": model, + "model": model_family.slug, "messages": messages, "stream": true, "tools": tools_json, diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 00762a8a67..38f390cb30 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -82,7 +82,7 @@ impl ModelClient { // Create the raw streaming connection first. let response_stream = stream_chat_completions( prompt, - &self.config.model, + &self.config.model_family, self.config.include_plan_tool, &self.client, &self.provider, @@ -127,13 +127,17 @@ impl ModelClient { let store = prompt.store && auth_mode != Some(AuthMode::ChatGPT); - let full_instructions = prompt.get_full_instructions(&self.config.model); + let full_instructions = prompt.get_full_instructions(&self.config.model_family); let tools_json = create_tools_json_for_responses_api( prompt, - &self.config.model, + &self.config.model_family, self.config.include_plan_tool, )?; - let reasoning = create_reasoning_param_for_request(&self.config, self.effort, self.summary); + let reasoning = create_reasoning_param_for_request( + &self.config.model_family, + self.effort, + self.summary, + ); // Request encrypted COT if we are not storing responses, // otherwise reasoning items will be referenced by ID diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index 6d9524cc92..58ec1c3f69 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -1,6 +1,7 @@ use crate::config_types::ReasoningEffort as ReasoningEffortConfig; use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; use crate::error::Result; +use crate::model_family::ModelFamily; use crate::models::ResponseItem; use crate::protocol::TokenUsage; use codex_apply_patch::APPLY_PATCH_TOOL_INSTRUCTIONS; @@ -42,13 +43,13 @@ pub struct Prompt { } impl Prompt { - pub(crate) fn get_full_instructions(&self, model: &str) -> Cow<'_, str> { + pub(crate) fn get_full_instructions(&self, model: &ModelFamily) -> Cow<'_, str> { let base = self .base_instructions_override .as_deref() .unwrap_or(BASE_INSTRUCTIONS); let mut sections: Vec<&str> = vec![base]; - if model.starts_with("gpt-4.1") { + if model.needs_special_apply_patch_instructions { sections.push(APPLY_PATCH_TOOL_INSTRUCTIONS); } Cow::Owned(sections.join("\n")) @@ -144,14 +145,12 @@ pub(crate) struct ResponsesApiRequest<'a> { pub(crate) include: Vec, } -use crate::config::Config; - pub(crate) fn create_reasoning_param_for_request( - config: &Config, + model_family: &ModelFamily, effort: ReasoningEffortConfig, summary: ReasoningSummaryConfig, ) -> Option { - if model_supports_reasoning_summaries(config) { + if model_family.supports_reasoning_summaries { let effort: Option = effort.into(); let effort = effort?; Some(Reasoning { @@ -163,27 +162,6 @@ pub(crate) fn create_reasoning_param_for_request( } } -pub fn model_supports_reasoning_summaries(config: &Config) -> bool { - // Currently, we hardcode this rule to decide whether to enable reasoning. - // We expect reasoning to apply only to OpenAI models, but we do not want - // users to have to mess with their config to disable reasoning for models - // that do not support it, such as `gpt-4.1`. - // - // Though if a user is using Codex with non-OpenAI models that, say, happen - // to start with "o", then they can set `model_reasoning_effort = "none"` in - // config.toml to disable reasoning. - // - // Converseley, if a user has a non-OpenAI provider that supports reasoning, - // they can set the top-level `model_supports_reasoning_summaries = true` - // config option to enable reasoning. - if config.model_supports_reasoning_summaries { - return true; - } - - let model = &config.model; - model.starts_with("o") || model.starts_with("codex") -} - pub(crate) struct ResponseStream { pub(crate) rx_event: mpsc::Receiver>, } @@ -198,6 +176,9 @@ impl Stream for ResponseStream { #[cfg(test)] mod tests { + #![allow(clippy::expect_used)] + use crate::model_family::find_family_for_model; + use super::*; #[test] @@ -207,7 +188,8 @@ mod tests { ..Default::default() }; let expected = format!("{BASE_INSTRUCTIONS}\n{APPLY_PATCH_TOOL_INSTRUCTIONS}"); - let full = prompt.get_full_instructions("gpt-4.1"); + let model_family = find_family_for_model("gpt-4.1").expect("known model slug"); + let full = prompt.get_full_instructions(&model_family); assert_eq!(full, expected); } } diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index b43dc56ba0..a0f36f4587 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -10,6 +10,8 @@ use crate::config_types::ShellEnvironmentPolicyToml; use crate::config_types::Tui; use crate::config_types::UriBasedFileOpener; use crate::flags::OPENAI_DEFAULT_MODEL; +use crate::model_family::ModelFamily; +use crate::model_family::find_family_for_model; use crate::model_provider_info::ModelProviderInfo; use crate::model_provider_info::built_in_model_providers; use crate::openai_model_info::get_model_info; @@ -33,6 +35,8 @@ pub struct Config { /// Optional override of model selection. pub model: String, + pub model_family: ModelFamily, + /// Size of the context window for the model, in tokens. pub model_context_window: Option, @@ -134,10 +138,6 @@ pub struct Config { /// request using the Responses API. pub model_reasoning_summary: ReasoningSummary, - /// When set to `true`, overrides the default heuristic and forces - /// `model_supports_reasoning_summaries()` to return `true`. - pub model_supports_reasoning_summaries: bool, - /// Base URL for requests to ChatGPT (as opposed to the OpenAI API). pub chatgpt_base_url: String, @@ -465,7 +465,19 @@ impl Config { .or(config_profile.model) .or(cfg.model) .unwrap_or_else(default_model); - let openai_model_info = get_model_info(&model); + let model_family = find_family_for_model(&model).unwrap_or_else(|| { + let supports_reasoning_summaries = + cfg.model_supports_reasoning_summaries.unwrap_or(false); + ModelFamily { + slug: model.clone(), + family: model.clone(), + needs_special_apply_patch_instructions: false, + supports_reasoning_summaries, + uses_local_shell_tool: false, + } + }); + + let openai_model_info = get_model_info(&model_family); let model_context_window = cfg .model_context_window .or_else(|| openai_model_info.as_ref().map(|info| info.context_window)); @@ -490,6 +502,7 @@ impl Config { let config = Self { model, + model_family, model_context_window, model_max_output_tokens, model_provider_id, @@ -527,10 +540,6 @@ impl Config { .or(cfg.model_reasoning_summary) .unwrap_or_default(), - model_supports_reasoning_summaries: cfg - .model_supports_reasoning_summaries - .unwrap_or(false), - chatgpt_base_url: config_profile .chatgpt_base_url .or(cfg.chatgpt_base_url) @@ -871,6 +880,7 @@ disable_response_storage = true assert_eq!( Config { model: "o3".to_string(), + model_family: find_family_for_model("o3").expect("known model slug"), model_context_window: Some(200_000), model_max_output_tokens: Some(100_000), model_provider_id: "openai".to_string(), @@ -893,7 +903,6 @@ disable_response_storage = true hide_agent_reasoning: false, model_reasoning_effort: ReasoningEffort::High, model_reasoning_summary: ReasoningSummary::Detailed, - model_supports_reasoning_summaries: false, chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), experimental_resume: None, base_instructions: None, @@ -921,6 +930,7 @@ disable_response_storage = true )?; let expected_gpt3_profile_config = Config { model: "gpt-3.5-turbo".to_string(), + model_family: find_family_for_model("gpt-3.5-turbo").expect("known model slug"), model_context_window: Some(16_385), model_max_output_tokens: Some(4_096), model_provider_id: "openai-chat-completions".to_string(), @@ -943,7 +953,6 @@ disable_response_storage = true hide_agent_reasoning: false, model_reasoning_effort: ReasoningEffort::default(), model_reasoning_summary: ReasoningSummary::default(), - model_supports_reasoning_summaries: false, chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), experimental_resume: None, base_instructions: None, @@ -986,6 +995,7 @@ disable_response_storage = true )?; let expected_zdr_profile_config = Config { model: "o3".to_string(), + model_family: find_family_for_model("o3").expect("known model slug"), model_context_window: Some(200_000), model_max_output_tokens: Some(100_000), model_provider_id: "openai".to_string(), @@ -1008,7 +1018,6 @@ disable_response_storage = true hide_agent_reasoning: false, model_reasoning_effort: ReasoningEffort::default(), model_reasoning_summary: ReasoningSummary::default(), - model_supports_reasoning_summaries: false, chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), experimental_resume: None, base_instructions: None, diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 4f083d9e56..f9c608b554 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -31,6 +31,7 @@ mod model_provider_info; pub use model_provider_info::ModelProviderInfo; pub use model_provider_info::WireApi; pub use model_provider_info::built_in_model_providers; +pub mod model_family; mod models; mod openai_model_info; mod openai_tools; @@ -47,5 +48,4 @@ mod user_notification; pub mod util; pub use apply_patch::CODEX_APPLY_PATCH_ARG1; -pub use client_common::model_supports_reasoning_summaries; pub use safety::get_platform_sandbox; diff --git a/codex-rs/core/src/model_family.rs b/codex-rs/core/src/model_family.rs new file mode 100644 index 0000000000..9bc61270ce --- /dev/null +++ b/codex-rs/core/src/model_family.rs @@ -0,0 +1,93 @@ +/// A model family is a group of models that share certain characteristics. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ModelFamily { + /// The full model slug used to derive this model family, e.g. + /// "gpt-4.1-2025-04-14". + pub slug: String, + + /// The model family name, e.g. "gpt-4.1". Note this should able to be used + /// with [`crate::openai_model_info::get_model_info`]. + pub family: String, + + /// True if the model needs additional instructions on how to use the + /// "virtual" `apply_patch` CLI. + pub needs_special_apply_patch_instructions: bool, + + // Whether the `reasoning` field can be set when making a request to this + // model family. Note it has `effort` and `summary` subfields (though + // `summary` is optional). + pub supports_reasoning_summaries: bool, + + // This should be set to true when the model expects a tool named + // "local_shell" to be provided. Its contract must be understood natively by + // the model such that its description can be omitted. + // See https://platform.openai.com/docs/guides/tools-local-shell + pub uses_local_shell_tool: bool, +} + +macro_rules! model_family { + ( + $slug:expr, $family:expr $(, $key:ident : $value:expr )* $(,)? + ) => {{ + // defaults + let mut mf = ModelFamily { + slug: $slug.to_string(), + family: $family.to_string(), + needs_special_apply_patch_instructions: false, + supports_reasoning_summaries: false, + uses_local_shell_tool: false, + }; + // apply overrides + $( + mf.$key = $value; + )* + Some(mf) + }}; +} + +macro_rules! simple_model_family { + ( + $slug:expr, $family:expr + ) => {{ + Some(ModelFamily { + slug: $slug.to_string(), + family: $family.to_string(), + needs_special_apply_patch_instructions: false, + supports_reasoning_summaries: false, + uses_local_shell_tool: false, + }) + }}; +} + +/// Returns a `ModelFamily` for the given model slug, or `None` if the slug +/// does not match any known model family. +pub fn find_family_for_model(slug: &str) -> Option { + if slug.starts_with("o3") { + model_family!( + slug, "o3", + supports_reasoning_summaries: true, + ) + } else if slug.starts_with("o4-mini") { + model_family!( + slug, "o4-mini", + supports_reasoning_summaries: true, + ) + } else if slug.starts_with("codex-mini-latest") { + model_family!( + slug, "codex-mini-latest", + supports_reasoning_summaries: true, + uses_local_shell_tool: true, + ) + } else if slug.starts_with("gpt-4.1") { + model_family!( + slug, "gpt-4.1", + needs_special_apply_patch_instructions: true, + ) + } else if slug.starts_with("gpt-4o") { + simple_model_family!(slug, "gpt-4o") + } else if slug.starts_with("gpt-3.5") { + simple_model_family!(slug, "gpt-3.5") + } else { + None + } +} diff --git a/codex-rs/core/src/openai_model_info.rs b/codex-rs/core/src/openai_model_info.rs index 9ffd831a91..51f028cbdd 100644 --- a/codex-rs/core/src/openai_model_info.rs +++ b/codex-rs/core/src/openai_model_info.rs @@ -1,3 +1,5 @@ +use crate::model_family::ModelFamily; + /// Metadata about a model, particularly OpenAI models. /// We may want to consider including details like the pricing for /// input tokens, output tokens, etc., though users will need to be able to @@ -14,8 +16,8 @@ pub(crate) struct ModelInfo { /// Note details such as what a model like gpt-4o is aliased to may be out of /// date. -pub(crate) fn get_model_info(name: &str) -> Option { - match name { +pub(crate) fn get_model_info(model_family: &ModelFamily) -> Option { + match model_family.slug.as_str() { // https://platform.openai.com/docs/models/o3 "o3" => Some(ModelInfo { context_window: 200_000, diff --git a/codex-rs/core/src/openai_tools.rs b/codex-rs/core/src/openai_tools.rs index 0f1e7d9ca7..305fa523bb 100644 --- a/codex-rs/core/src/openai_tools.rs +++ b/codex-rs/core/src/openai_tools.rs @@ -1,9 +1,9 @@ use serde::Serialize; use serde_json::json; use std::collections::BTreeMap; -use std::sync::LazyLock; use crate::client_common::Prompt; +use crate::model_family::ModelFamily; use crate::plan_tool::PLAN_TOOL; #[derive(Debug, Clone, Serialize)] @@ -42,8 +42,7 @@ pub(crate) enum JsonSchema { }, } -/// Tool usage specification -static DEFAULT_TOOLS: LazyLock> = LazyLock::new(|| { +fn create_shell_tool() -> OpenAiTool { let mut properties = BTreeMap::new(); properties.insert( "command".to_string(), @@ -54,7 +53,7 @@ static DEFAULT_TOOLS: LazyLock> = LazyLock::new(|| { properties.insert("workdir".to_string(), JsonSchema::String); properties.insert("timeout".to_string(), JsonSchema::Number); - vec![OpenAiTool::Function(ResponsesApiTool { + OpenAiTool::Function(ResponsesApiTool { name: "shell", description: "Runs a shell command, and returns its output.", strict: false, @@ -63,29 +62,26 @@ static DEFAULT_TOOLS: LazyLock> = LazyLock::new(|| { required: &["command"], additional_properties: false, }, - })] -}); - -static DEFAULT_CODEX_MODEL_TOOLS: LazyLock> = - LazyLock::new(|| vec![OpenAiTool::LocalShell {}]); + }) +} /// Returns JSON values that are compatible with Function Calling in the /// Responses API: /// https://platform.openai.com/docs/guides/function-calling?api-mode=responses pub(crate) fn create_tools_json_for_responses_api( prompt: &Prompt, - model: &str, + model_family: &ModelFamily, include_plan_tool: bool, ) -> crate::error::Result> { // Assemble tool list: built-in tools + any extra tools from the prompt. - let default_tools = if model.starts_with("codex") { - &DEFAULT_CODEX_MODEL_TOOLS - } else { - &DEFAULT_TOOLS - }; - let mut tools_json = Vec::with_capacity(default_tools.len() + prompt.extra_tools.len()); - for t in default_tools.iter() { - tools_json.push(serde_json::to_value(t)?); + let mut openai_tools = vec![create_shell_tool()]; + if model_family.uses_local_shell_tool { + openai_tools.push(OpenAiTool::LocalShell {}); + } + + let mut tools_json = Vec::with_capacity(openai_tools.len() + prompt.extra_tools.len() + 1); + for tool in openai_tools.iter() { + tools_json.push(serde_json::to_value(tool)?); } tools_json.extend( prompt @@ -107,13 +103,13 @@ pub(crate) fn create_tools_json_for_responses_api( /// https://platform.openai.com/docs/guides/function-calling?api-mode=chat pub(crate) fn create_tools_json_for_chat_completions_api( prompt: &Prompt, - model: &str, + model_family: &ModelFamily, include_plan_tool: bool, ) -> crate::error::Result> { // We start with the JSON for the Responses API and than rewrite it to match // the chat completions tool call format. let responses_api_tools_json = - create_tools_json_for_responses_api(prompt, model, include_plan_tool)?; + create_tools_json_for_responses_api(prompt, model_family, include_plan_tool)?; let tools_json = responses_api_tools_json .into_iter() .filter_map(|mut tool| { diff --git a/codex-rs/exec/src/event_processor.rs b/codex-rs/exec/src/event_processor.rs index 0f189f3fa2..0a2a141eca 100644 --- a/codex-rs/exec/src/event_processor.rs +++ b/codex-rs/exec/src/event_processor.rs @@ -3,7 +3,6 @@ use std::path::Path; use codex_common::summarize_sandbox_policy; use codex_core::WireApi; use codex_core::config::Config; -use codex_core::model_supports_reasoning_summaries; use codex_core::protocol::Event; pub(crate) enum CodexStatus { @@ -29,7 +28,7 @@ pub(crate) fn create_config_summary_entries(config: &Config) -> Vec<(&'static st ("sandbox", summarize_sandbox_policy(&config.sandbox_policy)), ]; if config.model_provider.wire_api == WireApi::Responses - && model_supports_reasoning_summaries(config) + && config.model_family.supports_reasoning_summaries { entries.push(( "reasoning effort", diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 17f0e683c0..2fb0eecb28 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -7,7 +7,6 @@ use codex_common::elapsed::format_duration; use codex_common::summarize_sandbox_policy; use codex_core::WireApi; use codex_core::config::Config; -use codex_core::model_supports_reasoning_summaries; use codex_core::plan_tool::PlanItemArg; use codex_core::plan_tool::StepStatus; use codex_core::plan_tool::UpdatePlanArgs; @@ -177,7 +176,7 @@ impl HistoryCell { ("sandbox", summarize_sandbox_policy(&config.sandbox_policy)), ]; if config.model_provider.wire_api == WireApi::Responses - && model_supports_reasoning_summaries(config) + && config.model_family.supports_reasoning_summaries { entries.push(( "reasoning effort", From d31e149cb1b4439f47393115d7a85b3c8ab8c90d Mon Sep 17 00:00:00 2001 From: Dylan Date: Tue, 5 Aug 2025 00:43:23 -0700 Subject: [PATCH 0017/1309] [prompt] Update prompt.md (#1839) ## Summary Additional clarifications to our prompt. Still very concise, but we'll continue to add more here. --- codex-rs/core/prompt.md | 49 ++++++++++++++++++++++++------- codex-rs/core/src/openai_tools.rs | 2 +- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/codex-rs/core/prompt.md b/codex-rs/core/prompt.md index f194eba4e2..d5d96a89b4 100644 --- a/codex-rs/core/prompt.md +++ b/codex-rs/core/prompt.md @@ -1,8 +1,21 @@ -Please resolve the user's task by editing and testing the code files in your current code execution session. -You are a deployed coding agent. -Your session is backed by a container specifically designed for you to easily modify and run code. -The repo(s) are already cloned in your working directory, and you must fully solve the problem for your answer to be considered correct. +You are operating as and within the Codex CLI, an open-source, terminal-based agentic coding assistant built by OpenAI. It wraps OpenAI models to enable natural language interaction with a local codebase. You are expected to be precise, safe, and helpful. +Your capabilities: +- Receive user prompts, project context, and files. +- Stream responses and emit function calls (e.g., shell commands, code edits). +- Run commands, like apply_patch, and manage user approvals based on policy. +- Work inside a workspace with sandboxing instructions specified by the policy described in (## Sandbox environment and approval instructions) + +Within this context, Codex refers to the open-source agentic coding interface (not the old Codex language model built by OpenAI). + +## General guidelines +As a deployed coding agent, please continue working on the user's task until their query is resolved, before ending your turn and yielding back to the user. Only terminate your turn when you are sure that the task is solved. If you are not sure about file content or codebase structure pertaining to the user's request, use your tools to read files and gather the relevant information. Do NOT guess or make up an answer. + +After a user sends their first message, you should immediately provide a brief message acknowledging their request to set the tone and expectation of future work to be done (no more than 8-10 words). This should be done before performing work like exploring the codebase, writing or reading files, or other tool calls needed to complete the task. Use a natural, collaborative tone similar to how a teammate would receive a task during a pair programming session. + +Please resolve the user's task by editing the code files in your current code execution session. Your session allows for you to modify and run code. The repo(s) are already cloned in your working directory, and you must fully solve the problem for your answer to be considered correct. + +### Task execution You MUST adhere to the following criteria when executing the task: - Working on the repo(s) in the current environment is allowed, even if they are proprietary. @@ -12,7 +25,7 @@ You MUST adhere to the following criteria when executing the task: - `user_instructions` are not part of the user's request, but guidance for how to complete the task. - Do not cite `user_instructions` back to the user unless a specific piece is relevant. - Do not use \`ls -R\`, \`find\`, or \`grep\` - these are slow in large repos. Use \`rg\` and \`rg --files\`. -- Use \`apply_patch\` to edit files: {"command":["apply_patch","*** Begin Patch\\n*** Update File: path/to/file.py\\n@@ def example():\\n- pass\\n+ return 123\\n*** End Patch"]} +- Use the \`apply_patch\` shell command to edit files: {"command":["apply_patch","*** Begin Patch\\n*** Update File: path/to/file.py\\n@@ def example():\\n- pass\\n+ return 123\\n*** End Patch"]} - If completing the user's task requires writing or modifying files: - Your code and final answer should follow these _CODING GUIDELINES_: - Fix the problem at the root cause rather than applying surface-level patches, when possible. @@ -35,12 +48,11 @@ You MUST adhere to the following criteria when executing the task: - If completing the user's task DOES NOT require writing or modifying files (e.g., the user asks a question about the code base): - Respond in a friendly tune as a remote teammate, who is knowledgeable, capable and eager to help with coding. - When your task involves writing or modifying files: - - Do NOT tell the user to "save the file" or "copy the code into a file" if you already created or modified the file using \`apply_patch\`. Instead, reference the file as already saved. + - Do NOT tell the user to "save the file" or "copy the code into a file" if you already created or modified the file using the `apply_patch` shell command. Instead, reference the file as already saved. - Do NOT show the full contents of large files you have already written, unless the user explicitly asks for them. -§ `apply-patch` Specification - -Your patch language is a stripped‑down, file‑oriented diff format designed to be easy to parse and safe to apply. You can think of it as a high‑level envelope: +## Using the shell command `apply_patch` to edit files +`apply_patch` is a shell command for editing files. Your patch language is a stripped‑down, file‑oriented diff format designed to be easy to parse and safe to apply. You can think of it as a high‑level envelope: *** Begin Patch [ one or more file sections ] @@ -92,14 +104,28 @@ It is important to remember: - You must include a header with your intended action (Add/Delete/Update) - You must prefix new lines with `+` even when creating a new file +- You must follow this schema exactly when providing a patch -You can invoke apply_patch like: +You can invoke apply_patch with the following shell command: ``` shell {"command":["apply_patch","*** Begin Patch\n*** Add File: hello.txt\n+Hello, world!\n*** End Patch\n"]} ``` -Plan updates +## Sandbox environment and approval instructions + +You are running in a sandboxed workspace backed by version control. The sandbox might be configured by the user to restrict certain behaviors, like accessing the internet or writing to files outside the current directory. + +Commands that are blocked by sandbox settings will be automatically sent to the user for approval. The result of the request will be returned (i.e. the command result, or the request denial). +The user also has an opportunity to approve the same command for the rest of the session. + +Guidance on running within the sandbox: +- When running commands that will likely require approval, attempt to use simple, precise commands, to reduce frequency of approval requests. +- When approval is denied or a command fails due to a permission error, do not retry the exact command in a different way. Move on and continue trying to address the user's request. + + +## Tools available +### Plan updates A tool named `update_plan` is available. Use it to keep an up‑to‑date, step‑by‑step plan for the task so you can follow your progress. When making your plans, keep in mind that you are a deployed coding agent - `update_plan` calls should not involve doing anything that you aren't capable of doing. For example, `update_plan` calls should NEVER contain tasks to merge your own pull requests. Only stop to ask the user if you genuinely need their feedback on a change. @@ -107,3 +133,4 @@ A tool named `update_plan` is available. Use it to keep an up‑to‑date, step - Whenever you finish a step, call `update_plan` again, marking the finished step as `completed` and the next step as `in_progress`. - If your plan needs to change, call `update_plan` with the revised steps and include an `explanation` describing the change. - When all steps are complete, make a final `update_plan` call with all steps marked `completed`. + diff --git a/codex-rs/core/src/openai_tools.rs b/codex-rs/core/src/openai_tools.rs index 305fa523bb..7d4bf4aa1a 100644 --- a/codex-rs/core/src/openai_tools.rs +++ b/codex-rs/core/src/openai_tools.rs @@ -55,7 +55,7 @@ fn create_shell_tool() -> OpenAiTool { OpenAiTool::Function(ResponsesApiTool { name: "shell", - description: "Runs a shell command, and returns its output.", + description: "Runs a shell command and returns its output", strict: false, parameters: JsonSchema::Object { properties, From e0303dbac06b6ead612107fa2144cf5b809bbe98 Mon Sep 17 00:00:00 2001 From: easong-openai Date: Tue, 5 Aug 2025 01:56:13 -0700 Subject: [PATCH 0018/1309] Rescue chat completion changes (#1846) https://github.com/openai/codex/pull/1835 has some messed up history. This adds support for streaming chat completions, which is useful for ollama. We should probably take a very skeptical eye to the code introduced in this PR. --------- Co-authored-by: Ahmed Ibrahim --- codex-rs/config.md | 13 + codex-rs/core/src/chat_completions.rs | 241 ++++++++++++++---- codex-rs/core/src/client.rs | 14 +- codex-rs/core/src/client_common.rs | 1 + codex-rs/core/src/codex.rs | 40 ++- codex-rs/core/src/config.rs | 12 + codex-rs/core/src/models.rs | 8 + codex-rs/core/src/protocol.rs | 16 ++ .../src/event_processor_with_human_output.rs | 34 +++ codex-rs/mcp-server/src/codex_tool_runner.rs | 4 +- codex-rs/mcp-server/src/conversation_loop.rs | 4 +- codex-rs/tui/src/bottom_pane/mod.rs | 148 ++++++++++- codex-rs/tui/src/chatwidget.rs | 69 ++++- 13 files changed, 547 insertions(+), 57 deletions(-) diff --git a/codex-rs/config.md b/codex-rs/config.md index c7dfe42a75..992fe1aacc 100644 --- a/codex-rs/config.md +++ b/codex-rs/config.md @@ -483,6 +483,19 @@ Setting `hide_agent_reasoning` to `true` suppresses these events in **both** the hide_agent_reasoning = true # defaults to false ``` +## show_raw_agent_reasoning + +Surfaces the model’s raw chain-of-thought ("raw reasoning content") when available. + +Notes: +- Only takes effect if the selected model/provider actually emits raw reasoning content. Many models do not. When unsupported, this option has no visible effect. +- Raw reasoning may include intermediate thoughts or sensitive context. Enable only if acceptable for your workflow. + +Example: +```toml +show_raw_agent_reasoning = true # defaults to false +``` + ## model_context_window The size of the context window for the model, in tokens. diff --git a/codex-rs/core/src/chat_completions.rs b/codex-rs/core/src/chat_completions.rs index 6aeccc5dfb..956dcebda9 100644 --- a/codex-rs/core/src/chat_completions.rs +++ b/codex-rs/core/src/chat_completions.rs @@ -23,6 +23,7 @@ use crate::error::CodexErr; use crate::error::Result; use crate::model_family::ModelFamily; use crate::models::ContentItem; +use crate::models::ReasoningItemContent; use crate::models::ResponseItem; use crate::openai_tools::create_tools_json_for_chat_completions_api; use crate::util::backoff; @@ -209,6 +210,8 @@ async fn process_chat_sse( } let mut fn_call_state = FunctionCallState::default(); + let mut assistant_text = String::new(); + let mut reasoning_text = String::new(); loop { let sse = match timeout(idle_timeout, stream.next()).await { @@ -237,6 +240,31 @@ async fn process_chat_sse( // OpenAI Chat streaming sends a literal string "[DONE]" when finished. if sse.data.trim() == "[DONE]" { + // Emit any finalized items before closing so downstream consumers receive + // terminal events for both assistant content and raw reasoning. + if !assistant_text.is_empty() { + let item = ResponseItem::Message { + role: "assistant".to_string(), + content: vec![ContentItem::OutputText { + text: std::mem::take(&mut assistant_text), + }], + id: None, + }; + let _ = tx_event.send(Ok(ResponseEvent::OutputItemDone(item))).await; + } + + if !reasoning_text.is_empty() { + let item = ResponseItem::Reasoning { + id: String::new(), + summary: Vec::new(), + content: Some(vec![ReasoningItemContent::ReasoningText { + text: std::mem::take(&mut reasoning_text), + }]), + encrypted_content: None, + }; + let _ = tx_event.send(Ok(ResponseEvent::OutputItemDone(item))).await; + } + let _ = tx_event .send(Ok(ResponseEvent::Completed { response_id: String::new(), @@ -256,26 +284,47 @@ async fn process_chat_sse( let choice_opt = chunk.get("choices").and_then(|c| c.get(0)); if let Some(choice) = choice_opt { - // Handle assistant content tokens. + // Handle assistant content tokens as streaming deltas. if let Some(content) = choice .get("delta") .and_then(|d| d.get("content")) .and_then(|c| c.as_str()) { - // Emit a delta so downstream consumers can stream text live. - let _ = tx_event - .send(Ok(ResponseEvent::OutputTextDelta(content.to_string()))) - .await; + if !content.is_empty() { + assistant_text.push_str(content); + let _ = tx_event + .send(Ok(ResponseEvent::OutputTextDelta(content.to_string()))) + .await; + } + } - let item = ResponseItem::Message { - role: "assistant".to_string(), - content: vec![ContentItem::OutputText { - text: content.to_string(), - }], - id: None, - }; + // Forward any reasoning/thinking deltas if present. + // Some providers stream `reasoning` as a plain string while others + // nest the text under an object (e.g. `{ "reasoning": { "text": "…" } }`). + if let Some(reasoning_val) = choice.get("delta").and_then(|d| d.get("reasoning")) { + let mut maybe_text = reasoning_val.as_str().map(|s| s.to_string()); - let _ = tx_event.send(Ok(ResponseEvent::OutputItemDone(item))).await; + if maybe_text.is_none() && reasoning_val.is_object() { + if let Some(s) = reasoning_val + .get("text") + .and_then(|t| t.as_str()) + .filter(|s| !s.is_empty()) + { + maybe_text = Some(s.to_string()); + } else if let Some(s) = reasoning_val + .get("content") + .and_then(|t| t.as_str()) + .filter(|s| !s.is_empty()) + { + maybe_text = Some(s.to_string()); + } + } + + if let Some(reasoning) = maybe_text { + let _ = tx_event + .send(Ok(ResponseEvent::ReasoningContentDelta(reasoning))) + .await; + } } // Handle streaming function / tool calls. @@ -312,7 +361,21 @@ async fn process_chat_sse( if let Some(finish_reason) = choice.get("finish_reason").and_then(|v| v.as_str()) { match finish_reason { "tool_calls" if fn_call_state.active => { - // Build the FunctionCall response item. + // First, flush the terminal raw reasoning so UIs can finalize + // the reasoning stream before any exec/tool events begin. + if !reasoning_text.is_empty() { + let item = ResponseItem::Reasoning { + id: String::new(), + summary: Vec::new(), + content: Some(vec![ReasoningItemContent::ReasoningText { + text: std::mem::take(&mut reasoning_text), + }]), + encrypted_content: None, + }; + let _ = tx_event.send(Ok(ResponseEvent::OutputItemDone(item))).await; + } + + // Then emit the FunctionCall response item. let item = ResponseItem::FunctionCall { id: None, name: fn_call_state.name.clone().unwrap_or_else(|| "".to_string()), @@ -320,11 +383,33 @@ async fn process_chat_sse( call_id: fn_call_state.call_id.clone().unwrap_or_else(String::new), }; - // Emit it downstream. let _ = tx_event.send(Ok(ResponseEvent::OutputItemDone(item))).await; } "stop" => { - // Regular turn without tool-call. + // Regular turn without tool-call. Emit the final assistant message + // as a single OutputItemDone so non-delta consumers see the result. + if !assistant_text.is_empty() { + let item = ResponseItem::Message { + role: "assistant".to_string(), + content: vec![ContentItem::OutputText { + text: std::mem::take(&mut assistant_text), + }], + id: None, + }; + let _ = tx_event.send(Ok(ResponseEvent::OutputItemDone(item))).await; + } + // Also emit a terminal Reasoning item so UIs can finalize raw reasoning. + if !reasoning_text.is_empty() { + let item = ResponseItem::Reasoning { + id: String::new(), + summary: Vec::new(), + content: Some(vec![ReasoningItemContent::ReasoningText { + text: std::mem::take(&mut reasoning_text), + }]), + encrypted_content: None, + }; + let _ = tx_event.send(Ok(ResponseEvent::OutputItemDone(item))).await; + } } _ => {} } @@ -362,10 +447,17 @@ async fn process_chat_sse( /// The adapter is intentionally *lossless*: callers who do **not** opt in via /// [`AggregateStreamExt::aggregate()`] keep receiving the original unmodified /// events. +#[derive(Copy, Clone, Eq, PartialEq)] +enum AggregateMode { + AggregatedOnly, + Streaming, +} pub(crate) struct AggregatedChatStream { inner: S, cumulative: String, - pending_completed: Option, + cumulative_reasoning: String, + pending: std::collections::VecDeque, + mode: AggregateMode, } impl Stream for AggregatedChatStream @@ -377,8 +469,8 @@ where fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.get_mut(); - // First, flush any buffered Completed event from the previous call. - if let Some(ev) = this.pending_completed.take() { + // First, flush any buffered events from the previous call. + if let Some(ev) = this.pending.pop_front() { return Poll::Ready(Some(Ok(ev))); } @@ -395,16 +487,21 @@ where let is_assistant_delta = matches!(&item, crate::models::ResponseItem::Message { role, .. } if role == "assistant"); if is_assistant_delta { - if let crate::models::ResponseItem::Message { content, .. } = &item { - if let Some(text) = content.iter().find_map(|c| match c { - crate::models::ContentItem::OutputText { text } => Some(text), - _ => None, - }) { - this.cumulative.push_str(text); + // Only use the final assistant message if we have not + // seen any deltas; otherwise, deltas already built the + // cumulative text and this would duplicate it. + if this.cumulative.is_empty() { + if let crate::models::ResponseItem::Message { content, .. } = &item { + if let Some(text) = content.iter().find_map(|c| match c { + crate::models::ContentItem::OutputText { text } => Some(text), + _ => None, + }) { + this.cumulative.push_str(text); + } } } - // Swallow partial assistant chunk; keep polling. + // Swallow assistant message here; emit on Completed. continue; } @@ -415,24 +512,50 @@ where response_id, token_usage, }))) => { + // Build any aggregated items in the correct order: Reasoning first, then Message. + let mut emitted_any = false; + + if !this.cumulative_reasoning.is_empty() + && matches!(this.mode, AggregateMode::AggregatedOnly) + { + let aggregated_reasoning = crate::models::ResponseItem::Reasoning { + id: String::new(), + summary: Vec::new(), + content: Some(vec![ + crate::models::ReasoningItemContent::ReasoningText { + text: std::mem::take(&mut this.cumulative_reasoning), + }, + ]), + encrypted_content: None, + }; + this.pending + .push_back(ResponseEvent::OutputItemDone(aggregated_reasoning)); + emitted_any = true; + } + if !this.cumulative.is_empty() { - let aggregated_item = crate::models::ResponseItem::Message { + let aggregated_message = crate::models::ResponseItem::Message { id: None, role: "assistant".to_string(), content: vec![crate::models::ContentItem::OutputText { text: std::mem::take(&mut this.cumulative), }], }; + this.pending + .push_back(ResponseEvent::OutputItemDone(aggregated_message)); + emitted_any = true; + } - // Buffer Completed so it is returned *after* the aggregated message. - this.pending_completed = Some(ResponseEvent::Completed { - response_id, - token_usage, + // Always emit Completed last when anything was aggregated. + if emitted_any { + this.pending.push_back(ResponseEvent::Completed { + response_id: response_id.clone(), + token_usage: token_usage.clone(), }); - - return Poll::Ready(Some(Ok(ResponseEvent::OutputItemDone( - aggregated_item, - )))); + // Return the first pending event now. + if let Some(ev) = this.pending.pop_front() { + return Poll::Ready(Some(Ok(ev))); + } } // Nothing aggregated – forward Completed directly. @@ -447,13 +570,27 @@ where continue; } Poll::Ready(Some(Ok(ResponseEvent::OutputTextDelta(delta)))) => { - // Forward deltas unchanged so callers can stream text - // live while still receiving a single aggregated - // OutputItemDone at the end of the turn. - return Poll::Ready(Some(Ok(ResponseEvent::OutputTextDelta(delta)))); + // Always accumulate deltas so we can emit a final OutputItemDone at Completed. + this.cumulative.push_str(&delta); + if matches!(this.mode, AggregateMode::Streaming) { + // In streaming mode, also forward the delta immediately. + return Poll::Ready(Some(Ok(ResponseEvent::OutputTextDelta(delta)))); + } else { + continue; + } } - Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryDelta(delta)))) => { - return Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryDelta(delta)))); + Poll::Ready(Some(Ok(ResponseEvent::ReasoningContentDelta(delta)))) => { + // Always accumulate reasoning deltas so we can emit a final Reasoning item at Completed. + this.cumulative_reasoning.push_str(&delta); + if matches!(this.mode, AggregateMode::Streaming) { + // In streaming mode, also forward the delta immediately. + return Poll::Ready(Some(Ok(ResponseEvent::ReasoningContentDelta(delta)))); + } else { + continue; + } + } + Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryDelta(_)))) => { + continue; } } } @@ -482,12 +619,24 @@ pub(crate) trait AggregateStreamExt: Stream> + Size /// } /// ``` fn aggregate(self) -> AggregatedChatStream { - AggregatedChatStream { - inner: self, - cumulative: String::new(), - pending_completed: None, - } + AggregatedChatStream::new(self, AggregateMode::AggregatedOnly) } } impl AggregateStreamExt for T where T: Stream> + Sized {} + +impl AggregatedChatStream { + fn new(inner: S, mode: AggregateMode) -> Self { + AggregatedChatStream { + inner, + cumulative: String::new(), + cumulative_reasoning: String::new(), + pending: std::collections::VecDeque::new(), + mode, + } + } + + pub(crate) fn streaming_mode(inner: S) -> Self { + Self::new(inner, AggregateMode::Streaming) + } +} diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 38f390cb30..514e683e53 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -92,7 +92,11 @@ impl ModelClient { // Wrap it with the aggregation adapter so callers see *only* // the final assistant message per turn (matching the // behaviour of the Responses API). - let mut aggregated = response_stream.aggregate(); + let mut aggregated = if self.config.show_raw_agent_reasoning { + crate::chat_completions::AggregatedChatStream::streaming_mode(response_stream) + } else { + response_stream.aggregate() + }; // Bridge the aggregated stream back into a standard // `ResponseStream` by forwarding events through a channel. @@ -437,6 +441,14 @@ async fn process_sse( } } } + "response.reasoning_text.delta" => { + if let Some(delta) = event.delta { + let event = ResponseEvent::ReasoningContentDelta(delta); + if tx_event.send(Ok(event)).await.is_err() { + return; + } + } + } "response.created" => { if event.response.is_some() { let _ = tx_event.send(Ok(ResponseEvent::Created {})).await; diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index 58ec1c3f69..8b845a52e6 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -72,6 +72,7 @@ pub enum ResponseEvent { }, OutputTextDelta(String), ReasoningSummaryDelta(String), + ReasoningContentDelta(String), } #[derive(Debug, Serialize)] diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 8d24356460..0ce0c4ea2b 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -56,6 +56,7 @@ use crate::mcp_tool_call::handle_mcp_tool_call; use crate::models::ContentItem; use crate::models::FunctionCallOutputPayload; use crate::models::LocalShellAction; +use crate::models::ReasoningItemContent; use crate::models::ReasoningItemReasoningSummary; use crate::models::ResponseInputItem; use crate::models::ResponseItem; @@ -66,6 +67,8 @@ use crate::protocol::AgentMessageDeltaEvent; use crate::protocol::AgentMessageEvent; use crate::protocol::AgentReasoningDeltaEvent; use crate::protocol::AgentReasoningEvent; +use crate::protocol::AgentReasoningRawContentDeltaEvent; +use crate::protocol::AgentReasoningRawContentEvent; use crate::protocol::ApplyPatchApprovalRequestEvent; use crate::protocol::AskForApproval; use crate::protocol::BackgroundEventEvent; @@ -227,6 +230,7 @@ pub(crate) struct Session { state: Mutex, codex_linux_sandbox_exe: Option, user_shell: shell::Shell, + show_raw_agent_reasoning: bool, } impl Session { @@ -822,6 +826,7 @@ async fn submission_loop( codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(), disable_response_storage, user_shell: default_shell, + show_raw_agent_reasoning: config.show_raw_agent_reasoning, })); // Patch restored state into the newly created session. @@ -1132,6 +1137,7 @@ async fn run_task(sess: Arc, sub_id: String, input: Vec) { ResponseItem::Reasoning { id, summary, + content, encrypted_content, }, None, @@ -1139,6 +1145,7 @@ async fn run_task(sess: Arc, sub_id: String, input: Vec) { items_to_record_in_conversation_history.push(ResponseItem::Reasoning { id: id.clone(), summary: summary.clone(), + content: content.clone(), encrypted_content: encrypted_content.clone(), }); } @@ -1392,6 +1399,17 @@ async fn try_run_turn( }; sess.tx_event.send(event).await.ok(); } + ResponseEvent::ReasoningContentDelta(delta) => { + if sess.show_raw_agent_reasoning { + let event = Event { + id: sub_id.to_string(), + msg: EventMsg::AgentReasoningRawContentDelta( + AgentReasoningRawContentDeltaEvent { delta }, + ), + }; + sess.tx_event.send(event).await.ok(); + } + } } } } @@ -1498,7 +1516,12 @@ async fn handle_response_item( } None } - ResponseItem::Reasoning { summary, .. } => { + ResponseItem::Reasoning { + id: _, + summary, + content, + encrypted_content: _, + } => { for item in summary { let text = match item { ReasoningItemReasoningSummary::SummaryText { text } => text, @@ -1509,6 +1532,21 @@ async fn handle_response_item( }; sess.tx_event.send(event).await.ok(); } + if sess.show_raw_agent_reasoning && content.is_some() { + let content = content.unwrap(); + for item in content { + let text = match item { + ReasoningItemContent::ReasoningText { text } => text, + }; + let event = Event { + id: sub_id.to_string(), + msg: EventMsg::AgentReasoningRawContent(AgentReasoningRawContentEvent { + text, + }), + }; + sess.tx_event.send(event).await.ok(); + } + } None } ResponseItem::FunctionCall { diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index a0f36f4587..d97d5ec13d 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -61,6 +61,10 @@ pub struct Config { /// users are only interested in the final agent responses. pub hide_agent_reasoning: bool, + /// When set to `true`, `AgentReasoningRawContentEvent` events will be shown in the UI/output. + /// Defaults to `false`. + pub show_raw_agent_reasoning: bool, + /// Disable server-side response storage (sends the full conversation /// context with every request). Currently necessary for OpenAI customers /// who have opted into Zero Data Retention (ZDR). @@ -325,6 +329,10 @@ pub struct ConfigToml { /// UI/output. Defaults to `false`. pub hide_agent_reasoning: Option, + /// When set to `true`, `AgentReasoningRawContentEvent` events will be shown in the UI/output. + /// Defaults to `false`. + pub show_raw_agent_reasoning: Option, + pub model_reasoning_effort: Option, pub model_reasoning_summary: Option, @@ -531,6 +539,7 @@ impl Config { codex_linux_sandbox_exe, hide_agent_reasoning: cfg.hide_agent_reasoning.unwrap_or(false), + show_raw_agent_reasoning: cfg.show_raw_agent_reasoning.unwrap_or(false), model_reasoning_effort: config_profile .model_reasoning_effort .or(cfg.model_reasoning_effort) @@ -901,6 +910,7 @@ disable_response_storage = true tui: Tui::default(), codex_linux_sandbox_exe: None, hide_agent_reasoning: false, + show_raw_agent_reasoning: false, model_reasoning_effort: ReasoningEffort::High, model_reasoning_summary: ReasoningSummary::Detailed, chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), @@ -951,6 +961,7 @@ disable_response_storage = true tui: Tui::default(), codex_linux_sandbox_exe: None, hide_agent_reasoning: false, + show_raw_agent_reasoning: false, model_reasoning_effort: ReasoningEffort::default(), model_reasoning_summary: ReasoningSummary::default(), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), @@ -1016,6 +1027,7 @@ disable_response_storage = true tui: Tui::default(), codex_linux_sandbox_exe: None, hide_agent_reasoning: false, + show_raw_agent_reasoning: false, model_reasoning_effort: ReasoningEffort::default(), model_reasoning_summary: ReasoningSummary::default(), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), diff --git a/codex-rs/core/src/models.rs b/codex-rs/core/src/models.rs index 91bfb3bc8c..fb48b53070 100644 --- a/codex-rs/core/src/models.rs +++ b/codex-rs/core/src/models.rs @@ -45,6 +45,8 @@ pub enum ResponseItem { Reasoning { id: String, summary: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + content: Option>, encrypted_content: Option, }, LocalShellCall { @@ -136,6 +138,12 @@ pub enum ReasoningItemReasoningSummary { SummaryText { text: String }, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ReasoningItemContent { + ReasoningText { text: String }, +} + impl From> for ResponseInputItem { fn from(items: Vec) -> Self { Self::Message { diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs index 82591a2c78..aa330f6bae 100644 --- a/codex-rs/core/src/protocol.rs +++ b/codex-rs/core/src/protocol.rs @@ -359,6 +359,12 @@ pub enum EventMsg { /// Agent reasoning delta event from agent. AgentReasoningDelta(AgentReasoningDeltaEvent), + /// Raw chain-of-thought from agent. + AgentReasoningRawContent(AgentReasoningRawContentEvent), + + /// Agent reasoning content delta event from agent. + AgentReasoningRawContentDelta(AgentReasoningRawContentDeltaEvent), + /// Ack the client's configure message. SessionConfigured(SessionConfiguredEvent), @@ -464,6 +470,16 @@ pub struct AgentReasoningEvent { pub text: String, } +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct AgentReasoningRawContentEvent { + pub text: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct AgentReasoningRawContentDeltaEvent { + pub delta: String, +} + #[derive(Debug, Clone, Deserialize, Serialize)] pub struct AgentReasoningDeltaEvent { pub delta: String, diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 7703c138fc..393ef4ab1b 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -5,6 +5,8 @@ use codex_core::plan_tool::UpdatePlanArgs; use codex_core::protocol::AgentMessageDeltaEvent; use codex_core::protocol::AgentMessageEvent; use codex_core::protocol::AgentReasoningDeltaEvent; +use codex_core::protocol::AgentReasoningRawContentDeltaEvent; +use codex_core::protocol::AgentReasoningRawContentEvent; use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::ErrorEvent; use codex_core::protocol::Event; @@ -55,8 +57,10 @@ pub(crate) struct EventProcessorWithHumanOutput { /// Whether to include `AgentReasoning` events in the output. show_agent_reasoning: bool, + show_raw_agent_reasoning: bool, answer_started: bool, reasoning_started: bool, + raw_reasoning_started: bool, last_message_path: Option, } @@ -81,8 +85,10 @@ impl EventProcessorWithHumanOutput { green: Style::new().green(), cyan: Style::new().cyan(), show_agent_reasoning: !config.hide_agent_reasoning, + show_raw_agent_reasoning: config.show_raw_agent_reasoning, answer_started: false, reasoning_started: false, + raw_reasoning_started: false, last_message_path, } } else { @@ -97,8 +103,10 @@ impl EventProcessorWithHumanOutput { green: Style::new(), cyan: Style::new(), show_agent_reasoning: !config.hide_agent_reasoning, + show_raw_agent_reasoning: config.show_raw_agent_reasoning, answer_started: false, reasoning_started: false, + raw_reasoning_started: false, last_message_path, } } @@ -203,6 +211,32 @@ impl EventProcessor for EventProcessorWithHumanOutput { #[allow(clippy::expect_used)] std::io::stdout().flush().expect("could not flush stdout"); } + EventMsg::AgentReasoningRawContent(AgentReasoningRawContentEvent { text }) => { + if !self.show_raw_agent_reasoning { + return CodexStatus::Running; + } + if !self.raw_reasoning_started { + print!("{text}"); + #[allow(clippy::expect_used)] + std::io::stdout().flush().expect("could not flush stdout"); + } else { + println!(); + self.raw_reasoning_started = false; + } + } + EventMsg::AgentReasoningRawContentDelta(AgentReasoningRawContentDeltaEvent { + delta, + }) => { + if !self.show_raw_agent_reasoning { + return CodexStatus::Running; + } + if !self.raw_reasoning_started { + self.raw_reasoning_started = true; + } + print!("{delta}"); + #[allow(clippy::expect_used)] + std::io::stdout().flush().expect("could not flush stdout"); + } EventMsg::AgentMessage(AgentMessageEvent { message }) => { // if answer_started is false, this means we haven't received any // delta. Thus, we need to print the message as a new answer. diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index 205dfa4631..b91c4a7609 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -252,7 +252,9 @@ async fn run_codex_tool_session_inner( EventMsg::AgentMessage(AgentMessageEvent { .. }) => { // TODO: think how we want to support this in the MCP } - EventMsg::TaskStarted + EventMsg::AgentReasoningRawContent(_) + | EventMsg::AgentReasoningRawContentDelta(_) + | EventMsg::TaskStarted | EventMsg::TokenCount(_) | EventMsg::AgentReasoning(_) | EventMsg::McpToolCallBegin(_) diff --git a/codex-rs/mcp-server/src/conversation_loop.rs b/codex-rs/mcp-server/src/conversation_loop.rs index 1db39a2306..80c34760c5 100644 --- a/codex-rs/mcp-server/src/conversation_loop.rs +++ b/codex-rs/mcp-server/src/conversation_loop.rs @@ -90,7 +90,9 @@ pub async fn run_conversation_loop( EventMsg::AgentMessage(AgentMessageEvent { .. }) => { // TODO: think how we want to support this in the MCP } - EventMsg::TaskStarted + EventMsg::AgentReasoningRawContent(_) + | EventMsg::AgentReasoningRawContentDelta(_) + | EventMsg::TaskStarted | EventMsg::TokenCount(_) | EventMsg::AgentReasoning(_) | EventMsg::McpToolCallBegin(_) diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index fde0b3bde8..cdb01ba06a 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -138,6 +138,11 @@ impl BottomPane<'_> { view.handle_key_event(self, key_event); if !view.is_complete() { self.active_view = Some(view); + } else if self.is_task_running { + let mut v = StatusIndicatorView::new(self.app_event_tx.clone()); + v.update_text("waiting for model".to_string()); + self.active_view = Some(Box::new(v)); + self.status_view_active = true; } self.request_redraw(); InputResult::None @@ -163,6 +168,12 @@ impl BottomPane<'_> { CancellationEvent::Handled => { if !view.is_complete() { self.active_view = Some(view); + } else if self.is_task_running { + // Modal aborted but task still running – restore status indicator. + let mut v = StatusIndicatorView::new(self.app_event_tx.clone()); + v.update_text("waiting for model".to_string()); + self.active_view = Some(Box::new(v)); + self.status_view_active = true; } self.show_ctrl_c_quit_hint(); } @@ -202,15 +213,20 @@ impl BottomPane<'_> { handled_by_view = true; } - // Fallback: if the current active view did not consume status updates, - // present an overlay above the composer. - if !handled_by_view { + // Fallback: if the current active view did not consume status updates + // and no modal view is active, present an overlay above the composer. + // If a modal is active, do NOT render the overlay to avoid drawing + // over the dialog. + if !handled_by_view && self.active_view.is_none() { if self.live_status.is_none() { self.live_status = Some(StatusIndicatorWidget::new(self.app_event_tx.clone())); } if let Some(status) = &mut self.live_status { status.update_text(text); } + } else if !handled_by_view { + // Ensure any previous overlay is cleared when a modal becomes active. + self.live_status = None; } self.request_redraw(); } @@ -296,6 +312,8 @@ impl BottomPane<'_> { // Otherwise create a new approval modal overlay. let modal = ApprovalModalView::new(request, self.app_event_tx.clone()); self.active_view = Some(Box::new(modal)); + // Hide any overlay status while a modal is visible. + self.live_status = None; self.status_view_active = false; self.request_redraw() } @@ -368,16 +386,18 @@ impl WidgetRef for &BottomPane<'_> { y_offset = y_offset.saturating_add(1); } if let Some(status) = &self.live_status { - let live_h = status.desired_height(area.width).min(area.height); + let live_h = status + .desired_height(area.width) + .min(area.height.saturating_sub(y_offset)); if live_h > 0 { let live_rect = Rect { x: area.x, - y: area.y, + y: area.y + y_offset, width: area.width, height: live_h, }; status.render_ref(live_rect, buf); - y_offset = live_h; + y_offset = y_offset.saturating_add(live_h); } } @@ -540,6 +560,122 @@ mod tests { ); } + #[test] + fn overlay_not_shown_above_approval_modal() { + let (tx_raw, _rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let mut pane = BottomPane::new(BottomPaneParams { + app_event_tx: tx, + has_input_focus: true, + enhanced_keys_supported: false, + }); + + // Create an approval modal (active view). + pane.push_approval_request(exec_request()); + // Attempt to update status; this should NOT create an overlay while modal is visible. + pane.update_status_text("running command".to_string()); + + // Render and verify the top row does not include the Working header overlay. + let area = Rect::new(0, 0, 60, 6); + let mut buf = Buffer::empty(area); + (&pane).render_ref(area, &mut buf); + + let mut r0 = String::new(); + for x in 0..area.width { + r0.push(buf[(x, 0)].symbol().chars().next().unwrap_or(' ')); + } + assert!( + !r0.contains("Working"), + "overlay Working header should not render above modal" + ); + } + + #[test] + fn composer_not_shown_after_denied_if_task_running() { + let (tx_raw, rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let mut pane = BottomPane::new(BottomPaneParams { + app_event_tx: tx.clone(), + has_input_focus: true, + enhanced_keys_supported: false, + }); + + // Start a running task so the status indicator replaces the composer. + pane.set_task_running(true); + pane.update_status_text("waiting for model".to_string()); + + // Push an approval modal (e.g., command approval) which should hide the status view. + pane.push_approval_request(exec_request()); + + // Simulate pressing 'n' (deny) on the modal. + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + pane.handle_key_event(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE)); + + // After denial, since the task is still running, the status indicator + // should be restored as the active view; the composer should NOT be visible. + assert!( + pane.status_view_active, + "status view should be active after denial" + ); + assert!(pane.active_view.is_some(), "active view should be present"); + + // Render and ensure the top row includes the Working header instead of the composer. + // Give the animation thread a moment to tick. + std::thread::sleep(std::time::Duration::from_millis(120)); + let area = Rect::new(0, 0, 40, 3); + let mut buf = Buffer::empty(area); + (&pane).render_ref(area, &mut buf); + let mut row0 = String::new(); + for x in 0..area.width { + row0.push(buf[(x, 0)].symbol().chars().next().unwrap_or(' ')); + } + assert!( + row0.contains("Working"), + "expected Working header after denial: {row0:?}" + ); + + // Drain the channel to avoid unused warnings. + drop(rx); + } + + #[test] + fn status_indicator_visible_during_command_execution() { + let (tx_raw, _rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let mut pane = BottomPane::new(BottomPaneParams { + app_event_tx: tx, + has_input_focus: true, + enhanced_keys_supported: false, + }); + + // Begin a task: show initial status. + pane.set_task_running(true); + pane.update_status_text("waiting for model".to_string()); + + // As a long-running command begins (post-approval), ensure the status + // indicator is visible while we wait for the command to run. + pane.update_status_text("running command".to_string()); + + // Allow some frames so the animation thread ticks. + std::thread::sleep(std::time::Duration::from_millis(120)); + + // Render and confirm the line contains the "Working" header. + let area = Rect::new(0, 0, 40, 3); + let mut buf = Buffer::empty(area); + (&pane).render_ref(area, &mut buf); + + let mut row0 = String::new(); + for x in 0..area.width { + row0.push(buf[(x, 0)].symbol().chars().next().unwrap_or(' ')); + } + assert!( + row0.contains("Working"), + "expected Working header: {row0:?}" + ); + } + #[test] fn bottom_padding_present_for_status_view() { let (tx_raw, _rx) = channel::(); diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index f63810b62a..94bd2f121b 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -9,6 +9,8 @@ use codex_core::protocol::AgentMessageDeltaEvent; use codex_core::protocol::AgentMessageEvent; use codex_core::protocol::AgentReasoningDeltaEvent; use codex_core::protocol::AgentReasoningEvent; +use codex_core::protocol::AgentReasoningRawContentDeltaEvent; +use codex_core::protocol::AgentReasoningRawContentEvent; use codex_core::protocol::ApplyPatchApprovalRequestEvent; use codex_core::protocol::ErrorEvent; use codex_core::protocol::Event; @@ -61,6 +63,7 @@ pub(crate) struct ChatWidget<'a> { initial_user_message: Option, token_usage: TokenUsage, reasoning_buffer: String, + content_buffer: String, // Buffer for streaming assistant answer text; we do not surface partial // We wait for the final AgentMessage event and then emit the full text // at once into scrollback so the history contains a single message. @@ -101,6 +104,24 @@ fn create_initial_user_message(text: String, image_paths: Vec) -> Optio } impl ChatWidget<'_> { + fn emit_stream_header(&mut self, kind: StreamKind) { + use ratatui::text::Line as RLine; + if self.stream_header_emitted { + return; + } + let header = match kind { + StreamKind::Reasoning => RLine::from("thinking".magenta().italic()), + StreamKind::Answer => RLine::from("codex".magenta().bold()), + }; + self.app_event_tx + .send(AppEvent::InsertHistory(vec![header])); + self.stream_header_emitted = true; + } + fn finalize_active_stream(&mut self) { + if let Some(kind) = self.current_stream { + self.finalize_stream(kind); + } + } pub(crate) fn new( config: Config, app_event_tx: AppEventSender, @@ -161,6 +182,7 @@ impl ChatWidget<'_> { ), token_usage: TokenUsage::default(), reasoning_buffer: String::new(), + content_buffer: String::new(), answer_buffer: String::new(), running_commands: HashMap::new(), live_builder: RowBuilder::new(80), @@ -276,6 +298,20 @@ impl ChatWidget<'_> { self.finalize_stream(StreamKind::Reasoning); self.request_redraw(); } + EventMsg::AgentReasoningRawContentDelta(AgentReasoningRawContentDeltaEvent { + delta, + }) => { + // Treat raw reasoning content the same as summarized reasoning for UI flow. + self.begin_stream(StreamKind::Reasoning); + self.reasoning_buffer.push_str(&delta); + self.stream_push_and_maybe_commit(&delta); + self.request_redraw(); + } + EventMsg::AgentReasoningRawContent(AgentReasoningRawContentEvent { text: _ }) => { + // Finalize the raw reasoning stream just like the summarized reasoning event. + self.finalize_stream(StreamKind::Reasoning); + self.request_redraw(); + } EventMsg::TaskStarted => { self.bottom_pane.clear_ctrl_c_quit_hint(); self.bottom_pane.set_task_running(true); @@ -299,6 +335,14 @@ impl ChatWidget<'_> { EventMsg::Error(ErrorEvent { message }) => { self.add_to_history(HistoryCell::new_error_event(message.clone())); self.bottom_pane.set_task_running(false); + self.bottom_pane.clear_live_ring(); + self.live_builder = RowBuilder::new(self.live_builder.width()); + self.current_stream = None; + self.stream_header_emitted = false; + self.answer_buffer.clear(); + self.reasoning_buffer.clear(); + self.content_buffer.clear(); + self.request_redraw(); } EventMsg::PlanUpdate(update) => { // Commit plan updates directly to history (no status-line preview). @@ -310,6 +354,7 @@ impl ChatWidget<'_> { cwd, reason, }) => { + self.finalize_active_stream(); // Log a background summary immediately so the history is chronological. let cmdline = strip_bash_lc_and_escape(&command); let text = format!( @@ -336,6 +381,7 @@ impl ChatWidget<'_> { reason, grant_root, }) => { + self.finalize_active_stream(); // ------------------------------------------------------------------ // Before we even prompt the user for approval we surface the patch // summary in the main conversation so that the dialog appears in a @@ -365,6 +411,10 @@ impl ChatWidget<'_> { command, cwd, }) => { + self.finalize_active_stream(); + // Ensure the status indicator is visible while the command runs. + self.bottom_pane + .update_status_text("running command".to_string()); self.running_commands.insert( call_id, RunningCommand { @@ -408,6 +458,7 @@ impl ChatWidget<'_> { call_id: _, invocation, }) => { + self.finalize_active_stream(); self.add_to_history(HistoryCell::new_active_mcp_tool_call(invocation)); } EventMsg::McpToolCallEnd(McpToolCallEndEvent { @@ -451,7 +502,9 @@ impl ChatWidget<'_> { /// Update the live log preview while a task is running. pub(crate) fn update_latest_log(&mut self, line: String) { - self.bottom_pane.update_status_text(line); + if self.bottom_pane.is_task_running() { + self.bottom_pane.update_status_text(line); + } } fn request_redraw(&mut self) { @@ -478,8 +531,15 @@ impl ChatWidget<'_> { if self.bottom_pane.is_task_running() { self.bottom_pane.clear_ctrl_c_quit_hint(); self.submit_op(Op::Interrupt); + self.bottom_pane.set_task_running(false); + self.bottom_pane.clear_live_ring(); + self.live_builder = RowBuilder::new(self.live_builder.width()); + self.current_stream = None; + self.stream_header_emitted = false; self.answer_buffer.clear(); self.reasoning_buffer.clear(); + self.content_buffer.clear(); + self.request_redraw(); CancellationEvent::Ignored } else if self.bottom_pane.ctrl_c_quit_hint_visible() { self.submit_op(Op::Shutdown); @@ -518,6 +578,12 @@ impl ChatWidget<'_> { impl ChatWidget<'_> { fn begin_stream(&mut self, kind: StreamKind) { + if let Some(current) = self.current_stream { + if current != kind { + self.finalize_stream(current); + } + } + if self.current_stream != Some(kind) { self.current_stream = Some(kind); self.stream_header_emitted = false; @@ -526,6 +592,7 @@ impl ChatWidget<'_> { // Ensure the waiting status is visible (composer replaced). self.bottom_pane .update_status_text("waiting for model".to_string()); + self.emit_stream_header(kind); } } From 9285350842fc87b4bf84f4788151707153270b6f Mon Sep 17 00:00:00 2001 From: easong-openai Date: Tue, 5 Aug 2025 11:31:11 -0700 Subject: [PATCH 0019/1309] Introduce `--oss` flag to use gpt-oss models (#1848) This adds support for easily running Codex backed by a local Ollama instance running our new open source models. See https://github.com/openai/gpt-oss for details. If you pass in `--oss` you'll be prompted to install/launch ollama, and it will automatically download the 20b model and attempt to use it. We'll likely want to expand this with some options later to make the experience smoother for users who can't run the 20b or want to run the 120b. Co-authored-by: Michael Bolin --- README.md | 36 ++ codex-rs/Cargo.lock | 19 + codex-rs/Cargo.toml | 1 + codex-rs/core/src/config.rs | 10 +- codex-rs/core/src/lib.rs | 1 + codex-rs/core/src/model_family.rs | 2 + codex-rs/core/src/model_provider_info.rs | 120 ++++-- codex-rs/exec/Cargo.toml | 1 + codex-rs/exec/src/cli.rs | 3 + codex-rs/exec/src/lib.rs | 24 +- codex-rs/mcp-server/src/codex_tool_config.rs | 2 + .../src/tool_handlers/create_conversation.rs | 2 + codex-rs/ollama/Cargo.toml | 32 ++ codex-rs/ollama/src/client.rs | 366 ++++++++++++++++++ codex-rs/ollama/src/lib.rs | 52 +++ codex-rs/ollama/src/parser.rs | 82 ++++ codex-rs/ollama/src/pull.rs | 147 +++++++ codex-rs/ollama/src/url.rs | 39 ++ codex-rs/tui/Cargo.toml | 1 + codex-rs/tui/src/cli.rs | 6 + codex-rs/tui/src/lib.rs | 22 +- 21 files changed, 924 insertions(+), 44 deletions(-) create mode 100644 codex-rs/ollama/Cargo.toml create mode 100644 codex-rs/ollama/src/client.rs create mode 100644 codex-rs/ollama/src/lib.rs create mode 100644 codex-rs/ollama/src/parser.rs create mode 100644 codex-rs/ollama/src/pull.rs create mode 100644 codex-rs/ollama/src/url.rs diff --git a/README.md b/README.md index c7f6a1d595..dd5e466252 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ This is the home of the **Codex CLI**, which is a coding agent from OpenAI that - [Quickstart](#quickstart) - [OpenAI API Users](#openai-api-users) - [OpenAI Plus/Pro Users](#openai-pluspro-users) + - [Using OpenAI Open Source Models](#using-open-source-models) - [Why Codex?](#why-codex) - [Security model & permissions](#security-model--permissions) - [Platform sandboxing details](#platform-sandboxing-details) @@ -186,6 +187,41 @@ they'll be committed to your working directory. --- +## Using Open Source Models + +Codex can run fully locally against an OpenAI‑compatible OSS host (like Ollama) using the `--oss` flag: + +- Interactive UI: + - codex --oss +- Non‑interactive (programmatic) mode: + - echo "Refactor utils" | codex exec --oss + +Model selection when using `--oss`: + +- If you omit `-m/--model`, Codex defaults to -m gpt-oss:20b and will verify it exists locally (downloading if needed). +- To pick a different size, pass one of: + - -m "gpt-oss:20b" + - -m "gpt-oss:120b" + +Point Codex at your own OSS host: + +- By default, `--oss` talks to http://localhost:11434/v1. +- To use a different host, set one of these environment variables before running Codex: + - CODEX_OSS_BASE_URL, for example: + - CODEX_OSS_BASE_URL="http://my-ollama.example.com:11434/v1" codex --oss -m gpt-oss:20b + - or CODEX_OSS_PORT (when the host is localhost): + - CODEX_OSS_PORT=11434 codex --oss + +Advanced: you can persist this in your config instead of environment variables by overriding the built‑in `oss` provider in `~/.codex/config.toml`: + +```toml +[model_providers.oss] +name = "Open Source" +base_url = "http://my-ollama.example.com:11434/v1" +``` + +--- + ## Why Codex? Codex CLI is built for developers who already **live in the terminal** and want diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 2e20a7d624..4e21baf7f5 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -729,6 +729,7 @@ dependencies = [ "codex-arg0", "codex-common", "codex-core", + "codex-ollama", "owo-colors", "predicates", "serde_json", @@ -838,6 +839,23 @@ dependencies = [ "wiremock", ] +[[package]] +name = "codex-ollama" +version = "0.0.0" +dependencies = [ + "async-stream", + "bytes", + "codex-core", + "futures", + "reqwest", + "serde_json", + "tempfile", + "tokio", + "toml 0.9.4", + "tracing", + "wiremock", +] + [[package]] name = "codex-tui" version = "0.0.0" @@ -852,6 +870,7 @@ dependencies = [ "codex-core", "codex-file-search", "codex-login", + "codex-ollama", "color-eyre", "crossterm", "image", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 0f8085c7e5..0ed8852228 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -14,6 +14,7 @@ members = [ "mcp-client", "mcp-server", "mcp-types", + "ollama", "tui", ] resolver = "2" diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index d97d5ec13d..e62fcc39e2 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -385,6 +385,8 @@ pub struct ConfigOverrides { pub codex_linux_sandbox_exe: Option, pub base_instructions: Option, pub include_plan_tool: Option, + pub default_disable_response_storage: Option, + pub default_show_raw_agent_reasoning: Option, } impl Config { @@ -408,6 +410,8 @@ impl Config { codex_linux_sandbox_exe, base_instructions, include_plan_tool, + default_disable_response_storage, + default_show_raw_agent_reasoning, } = overrides; let config_profile = match config_profile_key.as_ref().or(cfg.profile.as_ref()) { @@ -525,6 +529,7 @@ impl Config { disable_response_storage: config_profile .disable_response_storage .or(cfg.disable_response_storage) + .or(default_disable_response_storage) .unwrap_or(false), notify: cfg.notify, user_instructions, @@ -539,7 +544,10 @@ impl Config { codex_linux_sandbox_exe, hide_agent_reasoning: cfg.hide_agent_reasoning.unwrap_or(false), - show_raw_agent_reasoning: cfg.show_raw_agent_reasoning.unwrap_or(false), + show_raw_agent_reasoning: cfg + .show_raw_agent_reasoning + .or(default_show_raw_agent_reasoning) + .unwrap_or(false), model_reasoning_effort: config_profile .model_reasoning_effort .or(cfg.model_reasoning_effort) diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index f9c608b554..965cb77bf1 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -28,6 +28,7 @@ mod mcp_connection_manager; mod mcp_tool_call; mod message_history; mod model_provider_info; +pub use model_provider_info::BUILT_IN_OSS_MODEL_PROVIDER_ID; pub use model_provider_info::ModelProviderInfo; pub use model_provider_info::WireApi; pub use model_provider_info::built_in_model_providers; diff --git a/codex-rs/core/src/model_family.rs b/codex-rs/core/src/model_family.rs index 9bc61270ce..7c4a9de6c2 100644 --- a/codex-rs/core/src/model_family.rs +++ b/codex-rs/core/src/model_family.rs @@ -85,6 +85,8 @@ pub fn find_family_for_model(slug: &str) -> Option { ) } else if slug.starts_with("gpt-4o") { simple_model_family!(slug, "gpt-4o") + } else if slug.starts_with("gpt-oss") { + simple_model_family!(slug, "gpt-oss") } else if slug.starts_with("gpt-3.5") { simple_model_family!(slug, "gpt-3.5") } else { diff --git a/codex-rs/core/src/model_provider_info.rs b/codex-rs/core/src/model_provider_info.rs index 49478660f4..595f05ef75 100644 --- a/codex-rs/core/src/model_provider_info.rs +++ b/codex-rs/core/src/model_provider_info.rs @@ -226,53 +226,93 @@ impl ModelProviderInfo { } } +const DEFAULT_OLLAMA_PORT: u32 = 11434; + +pub const BUILT_IN_OSS_MODEL_PROVIDER_ID: &str = "oss"; + /// Built-in default provider list. pub fn built_in_model_providers() -> HashMap { use ModelProviderInfo as P; - // We do not want to be in the business of adjucating which third-party - // providers are bundled with Codex CLI, so we only include the OpenAI - // provider by default. Users are encouraged to add to `model_providers` - // in config.toml to add their own providers. - [( - "openai", - P { - name: "OpenAI".into(), - // Allow users to override the default OpenAI endpoint by - // exporting `OPENAI_BASE_URL`. This is useful when pointing - // Codex at a proxy, mock server, or Azure-style deployment - // without requiring a full TOML override for the built-in - // OpenAI provider. - base_url: std::env::var("OPENAI_BASE_URL") + // These CODEX_OSS_ environment variables are experimental: we may + // switch to reading values from config.toml instead. + let codex_oss_base_url = match std::env::var("CODEX_OSS_BASE_URL") + .ok() + .filter(|v| !v.trim().is_empty()) + { + Some(url) => url, + None => format!( + "http://localhost:{port}/v1", + port = std::env::var("CODEX_OSS_PORT") .ok() - .filter(|v| !v.trim().is_empty()), - env_key: None, - env_key_instructions: None, - wire_api: WireApi::Responses, - query_params: None, - http_headers: Some( - [("version".to_string(), env!("CARGO_PKG_VERSION").to_string())] + .filter(|v| !v.trim().is_empty()) + .and_then(|v| v.parse::().ok()) + .unwrap_or(DEFAULT_OLLAMA_PORT) + ), + }; + + // We do not want to be in the business of adjucating which third-party + // providers are bundled with Codex CLI, so we only include the OpenAI and + // open source ("oss") providers by default. Users are encouraged to add to + // `model_providers` in config.toml to add their own providers. + [ + ( + "openai", + P { + name: "OpenAI".into(), + // Allow users to override the default OpenAI endpoint by + // exporting `OPENAI_BASE_URL`. This is useful when pointing + // Codex at a proxy, mock server, or Azure-style deployment + // without requiring a full TOML override for the built-in + // OpenAI provider. + base_url: std::env::var("OPENAI_BASE_URL") + .ok() + .filter(|v| !v.trim().is_empty()), + env_key: None, + env_key_instructions: None, + wire_api: WireApi::Responses, + query_params: None, + http_headers: Some( + [("version".to_string(), env!("CARGO_PKG_VERSION").to_string())] + .into_iter() + .collect(), + ), + env_http_headers: Some( + [ + ( + "OpenAI-Organization".to_string(), + "OPENAI_ORGANIZATION".to_string(), + ), + ("OpenAI-Project".to_string(), "OPENAI_PROJECT".to_string()), + ] .into_iter() .collect(), - ), - env_http_headers: Some( - [ - ( - "OpenAI-Organization".to_string(), - "OPENAI_ORGANIZATION".to_string(), - ), - ("OpenAI-Project".to_string(), "OPENAI_PROJECT".to_string()), - ] - .into_iter() - .collect(), - ), - // Use global defaults for retry/timeout unless overridden in config.toml. - request_max_retries: None, - stream_max_retries: None, - stream_idle_timeout_ms: None, - requires_auth: true, - }, - )] + ), + // Use global defaults for retry/timeout unless overridden in config.toml. + request_max_retries: None, + stream_max_retries: None, + stream_idle_timeout_ms: None, + requires_auth: true, + }, + ), + ( + BUILT_IN_OSS_MODEL_PROVIDER_ID, + P { + name: "Open Source".into(), + base_url: Some(codex_oss_base_url), + env_key: None, + env_key_instructions: None, + wire_api: WireApi::Chat, + query_params: None, + http_headers: None, + env_http_headers: None, + request_max_retries: None, + stream_max_retries: None, + stream_idle_timeout_ms: None, + requires_auth: false, + }, + ), + ] .into_iter() .map(|(k, v)| (k.to_string(), v)) .collect() diff --git a/codex-rs/exec/Cargo.toml b/codex-rs/exec/Cargo.toml index cd521410b1..aee480d7b4 100644 --- a/codex-rs/exec/Cargo.toml +++ b/codex-rs/exec/Cargo.toml @@ -25,6 +25,7 @@ codex-common = { path = "../common", features = [ "sandbox_summary", ] } codex-core = { path = "../core" } +codex-ollama = { path = "../ollama" } owo-colors = "4.2.0" serde_json = "1" shlex = "1.3.0" diff --git a/codex-rs/exec/src/cli.rs b/codex-rs/exec/src/cli.rs index 53af25c7e9..ea659e3252 100644 --- a/codex-rs/exec/src/cli.rs +++ b/codex-rs/exec/src/cli.rs @@ -14,6 +14,9 @@ pub struct Cli { #[arg(long, short = 'm')] pub model: Option, + #[arg(long = "oss", default_value_t = false)] + pub oss: bool, + /// Select the sandbox policy to use when executing model-generated shell /// commands. #[arg(long = "sandbox", short = 's')] diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index ce4d7f65cc..c1af4f5b45 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -9,6 +9,7 @@ use std::path::PathBuf; use std::sync::Arc; pub use cli::Cli; +use codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID; use codex_core::codex_wrapper::CodexConversation; use codex_core::codex_wrapper::{self}; use codex_core::config::Config; @@ -35,6 +36,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any let Cli { images, model, + oss, config_profile, full_auto, dangerously_bypass_approvals_and_sandbox, @@ -114,6 +116,24 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any sandbox_mode_cli_arg.map(Into::::into) }; + // When using `--oss`, let the bootstrapper pick the model (defaulting to + // gpt-oss:20b) and ensure it is present locally. Also, force the built‑in + // `oss` model provider. + let model_provider_override = if oss { + Some(BUILT_IN_OSS_MODEL_PROVIDER_ID.to_owned()) + } else { + None + }; + let model = if oss { + Some( + codex_ollama::ensure_oss_ready(model.clone()) + .await + .map_err(|e| anyhow::anyhow!("OSS setup failed: {e}"))?, + ) + } else { + model + }; + // Load configuration and determine approval policy let overrides = ConfigOverrides { model, @@ -123,10 +143,12 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any approval_policy: Some(AskForApproval::Never), sandbox_mode, cwd: cwd.map(|p| p.canonicalize().unwrap_or(p)), - model_provider: None, + model_provider: model_provider_override, codex_linux_sandbox_exe, base_instructions: None, include_plan_tool: None, + default_disable_response_storage: oss.then_some(true), + default_show_raw_agent_reasoning: oss.then_some(true), }; // Parse `-c` overrides. let cli_kv_overrides = match config_overrides.parse_overrides() { diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs index 877d0e05f7..f1a502bbb3 100644 --- a/codex-rs/mcp-server/src/codex_tool_config.rs +++ b/codex-rs/mcp-server/src/codex_tool_config.rs @@ -158,6 +158,8 @@ impl CodexToolCallParam { codex_linux_sandbox_exe, base_instructions, include_plan_tool, + default_disable_response_storage: None, + default_show_raw_agent_reasoning: None, }; let cli_overrides = cli_overrides diff --git a/codex-rs/mcp-server/src/tool_handlers/create_conversation.rs b/codex-rs/mcp-server/src/tool_handlers/create_conversation.rs index 28a8965115..c1f4035663 100644 --- a/codex-rs/mcp-server/src/tool_handlers/create_conversation.rs +++ b/codex-rs/mcp-server/src/tool_handlers/create_conversation.rs @@ -59,6 +59,8 @@ pub(crate) async fn handle_create_conversation( codex_linux_sandbox_exe: None, base_instructions, include_plan_tool: None, + default_disable_response_storage: None, + default_show_raw_agent_reasoning: None, }; let cfg: CodexConfig = match CodexConfig::load_with_cli_overrides(cli_overrides, overrides) { diff --git a/codex-rs/ollama/Cargo.toml b/codex-rs/ollama/Cargo.toml new file mode 100644 index 0000000000..ead9a06494 --- /dev/null +++ b/codex-rs/ollama/Cargo.toml @@ -0,0 +1,32 @@ +[package] +edition = "2024" +name = "codex-ollama" +version = { workspace = true } + +[lib] +name = "codex_ollama" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +async-stream = "0.3" +bytes = "1.10.1" +codex-core = { path = "../core" } +futures = "0.3" +reqwest = { version = "0.12", features = ["json", "stream"] } +serde_json = "1" +tokio = { version = "1", features = [ + "io-std", + "macros", + "process", + "rt-multi-thread", + "signal", +] } +toml = "0.9.2" +tracing = { version = "0.1.41", features = ["log"] } +wiremock = "0.6" + +[dev-dependencies] +tempfile = "3" diff --git a/codex-rs/ollama/src/client.rs b/codex-rs/ollama/src/client.rs new file mode 100644 index 0000000000..8a15039fad --- /dev/null +++ b/codex-rs/ollama/src/client.rs @@ -0,0 +1,366 @@ +use bytes::BytesMut; +use futures::StreamExt; +use futures::stream::BoxStream; +use serde_json::Value as JsonValue; +use std::collections::VecDeque; +use std::io; + +use codex_core::WireApi; + +use crate::parser::pull_events_from_value; +use crate::pull::PullEvent; +use crate::pull::PullProgressReporter; +use crate::url::base_url_to_host_root; +use crate::url::is_openai_compatible_base_url; + +/// Client for interacting with a local Ollama instance. +pub struct OllamaClient { + client: reqwest::Client, + host_root: String, + uses_openai_compat: bool, +} + +impl OllamaClient { + pub fn from_oss_provider() -> Self { + #![allow(clippy::expect_used)] + // Use the built-in OSS provider's base URL. + let built_in_model_providers = codex_core::built_in_model_providers(); + let provider = built_in_model_providers + .get(codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID) + .expect("oss provider must exist"); + let base_url = provider + .base_url + .as_ref() + .expect("oss provider must have a base_url"); + Self::from_provider(base_url, provider.wire_api) + } + + /// Construct a client for the built‑in open‑source ("oss") model provider + /// and verify that a local Ollama server is reachable. If no server is + /// detected, returns an error with helpful installation/run instructions. + pub async fn try_from_oss_provider() -> io::Result { + let client = Self::from_oss_provider(); + if client.probe_server().await? { + Ok(client) + } else { + Err(io::Error::other( + "No running Ollama server detected. Start it with: `ollama serve` (after installing). Install instructions: https://github.com/ollama/ollama?tab=readme-ov-file#ollama", + )) + } + } + + /// Build a client from a provider definition. Falls back to the default + /// local URL if no base_url is configured. + fn from_provider(base_url: &str, wire_api: WireApi) -> Self { + let uses_openai_compat = is_openai_compatible_base_url(base_url) + || matches!(wire_api, WireApi::Chat) && is_openai_compatible_base_url(base_url); + let host_root = base_url_to_host_root(base_url); + let client = reqwest::Client::builder() + .connect_timeout(std::time::Duration::from_secs(5)) + .build() + .unwrap_or_else(|_| reqwest::Client::new()); + Self { + client, + host_root, + uses_openai_compat, + } + } + + /// Low-level constructor given a raw host root, e.g. "http://localhost:11434". + #[cfg(test)] + fn from_host_root(host_root: impl Into) -> Self { + let client = reqwest::Client::builder() + .connect_timeout(std::time::Duration::from_secs(5)) + .build() + .unwrap_or_else(|_| reqwest::Client::new()); + Self { + client, + host_root: host_root.into(), + uses_openai_compat: false, + } + } + + /// Probe whether the server is reachable by hitting the appropriate health endpoint. + pub async fn probe_server(&self) -> io::Result { + let url = if self.uses_openai_compat { + format!("{}/v1/models", self.host_root.trim_end_matches('/')) + } else { + format!("{}/api/tags", self.host_root.trim_end_matches('/')) + }; + let resp = self.client.get(url).send().await; + Ok(matches!(resp, Ok(r) if r.status().is_success())) + } + + /// Return the list of model names known to the local Ollama instance. + pub async fn fetch_models(&self) -> io::Result> { + let tags_url = format!("{}/api/tags", self.host_root.trim_end_matches('/')); + let resp = self + .client + .get(tags_url) + .send() + .await + .map_err(io::Error::other)?; + if !resp.status().is_success() { + return Ok(Vec::new()); + } + let val = resp.json::().await.map_err(io::Error::other)?; + let names = val + .get("models") + .and_then(|m| m.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.get("name").and_then(|n| n.as_str())) + .map(|s| s.to_string()) + .collect::>() + }) + .unwrap_or_default(); + Ok(names) + } + + /// Start a model pull and emit streaming events. The returned stream ends when + /// a Success event is observed or the server closes the connection. + pub async fn pull_model_stream( + &self, + model: &str, + ) -> io::Result> { + let url = format!("{}/api/pull", self.host_root.trim_end_matches('/')); + let resp = self + .client + .post(url) + .json(&serde_json::json!({"model": model, "stream": true})) + .send() + .await + .map_err(io::Error::other)?; + if !resp.status().is_success() { + return Err(io::Error::other(format!( + "failed to start pull: HTTP {}", + resp.status() + ))); + } + + let mut stream = resp.bytes_stream(); + let mut buf = BytesMut::new(); + let _pending: VecDeque = VecDeque::new(); + + // Using an async stream adaptor backed by unfold-like manual loop. + let s = async_stream::stream! { + while let Some(chunk) = stream.next().await { + match chunk { + Ok(bytes) => { + buf.extend_from_slice(&bytes); + while let Some(pos) = buf.iter().position(|b| *b == b'\n') { + let line = buf.split_to(pos + 1); + if let Ok(text) = std::str::from_utf8(&line) { + let text = text.trim(); + if text.is_empty() { continue; } + if let Ok(value) = serde_json::from_str::(text) { + for ev in pull_events_from_value(&value) { yield ev; } + if let Some(err_msg) = value.get("error").and_then(|e| e.as_str()) { + yield PullEvent::Error(err_msg.to_string()); + return; + } + if let Some(status) = value.get("status").and_then(|s| s.as_str()) { + if status == "success" { yield PullEvent::Success; return; } + } + } + } + } + } + Err(_) => { + // Connection error: end the stream. + return; + } + } + } + }; + + Ok(Box::pin(s)) + } + + /// High-level helper to pull a model and drive a progress reporter. + pub async fn pull_with_reporter( + &self, + model: &str, + reporter: &mut dyn PullProgressReporter, + ) -> io::Result<()> { + reporter.on_event(&PullEvent::Status(format!("Pulling model {model}...")))?; + let mut stream = self.pull_model_stream(model).await?; + while let Some(event) = stream.next().await { + reporter.on_event(&event)?; + match event { + PullEvent::Success => { + return Ok(()); + } + PullEvent::Error(err) => { + // Emperically, ollama returns a 200 OK response even when + // the output stream includes an error message. Verify with: + // + // `curl -i http://localhost:11434/api/pull -d '{ "model": "foobarbaz" }'` + // + // As such, we have to check the event stream, not the + // HTTP response status, to determine whether to return Err. + return Err(io::Error::other(format!("Pull failed: {err}"))); + } + PullEvent::ChunkProgress { .. } | PullEvent::Status(_) => { + continue; + } + } + } + Err(io::Error::other( + "Pull stream ended unexpectedly without success.", + )) + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::expect_used, clippy::unwrap_used)] + use super::*; + + /// Simple RAII guard to set an environment variable for the duration of a test + /// and restore the previous value (or remove it) on drop to avoid cross-test + /// interference. + struct EnvVarGuard { + key: String, + prev: Option, + } + impl EnvVarGuard { + fn set(key: &str, value: String) -> Self { + let prev = std::env::var(key).ok(); + // set_var is safe but we mirror existing tests that use an unsafe block + // to silence edition lints around global mutation during tests. + unsafe { std::env::set_var(key, value) }; + Self { + key: key.to_string(), + prev, + } + } + } + impl Drop for EnvVarGuard { + fn drop(&mut self) { + match &self.prev { + Some(v) => unsafe { std::env::set_var(&self.key, v) }, + None => unsafe { std::env::remove_var(&self.key) }, + } + } + } + + // Happy-path tests using a mock HTTP server; skip if sandbox network is disabled. + #[tokio::test] + async fn test_fetch_models_happy_path() { + if std::env::var(codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { + tracing::info!( + "{} is set; skipping test_fetch_models_happy_path", + codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR + ); + return; + } + + let server = wiremock::MockServer::start().await; + wiremock::Mock::given(wiremock::matchers::method("GET")) + .and(wiremock::matchers::path("/api/tags")) + .respond_with( + wiremock::ResponseTemplate::new(200).set_body_raw( + serde_json::json!({ + "models": [ {"name": "llama3.2:3b"}, {"name":"mistral"} ] + }) + .to_string(), + "application/json", + ), + ) + .mount(&server) + .await; + + let client = OllamaClient::from_host_root(server.uri()); + let models = client.fetch_models().await.expect("fetch models"); + assert!(models.contains(&"llama3.2:3b".to_string())); + assert!(models.contains(&"mistral".to_string())); + } + + #[tokio::test] + async fn test_probe_server_happy_path_openai_compat_and_native() { + if std::env::var(codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { + tracing::info!( + "{} set; skipping test_probe_server_happy_path_openai_compat_and_native", + codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR + ); + return; + } + + let server = wiremock::MockServer::start().await; + + // Native endpoint + wiremock::Mock::given(wiremock::matchers::method("GET")) + .and(wiremock::matchers::path("/api/tags")) + .respond_with(wiremock::ResponseTemplate::new(200)) + .mount(&server) + .await; + let native = OllamaClient::from_host_root(server.uri()); + assert!(native.probe_server().await.expect("probe native")); + + // OpenAI compatibility endpoint + wiremock::Mock::given(wiremock::matchers::method("GET")) + .and(wiremock::matchers::path("/v1/models")) + .respond_with(wiremock::ResponseTemplate::new(200)) + .mount(&server) + .await; + // Ensure the built-in OSS provider points at our mock server for this test + // to avoid depending on any globally configured environment from other tests. + let _guard = EnvVarGuard::set("CODEX_OSS_BASE_URL", format!("{}/v1", server.uri())); + let ollama_client = OllamaClient::from_oss_provider(); + assert!(ollama_client.probe_server().await.expect("probe compat")); + } + + #[tokio::test] + async fn test_try_from_oss_provider_ok_when_server_running() { + if std::env::var(codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { + tracing::info!( + "{} set; skipping test_try_from_oss_provider_ok_when_server_running", + codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR + ); + return; + } + + let server = wiremock::MockServer::start().await; + // Configure built‑in `oss` provider to point at this mock server. + // set_var is unsafe on Rust 2024 edition; use unsafe block in tests. + let _guard = EnvVarGuard::set("CODEX_OSS_BASE_URL", format!("{}/v1", server.uri())); + + // OpenAI‑compat models endpoint responds OK. + wiremock::Mock::given(wiremock::matchers::method("GET")) + .and(wiremock::matchers::path("/v1/models")) + .respond_with(wiremock::ResponseTemplate::new(200)) + .mount(&server) + .await; + + let _client = OllamaClient::try_from_oss_provider() + .await + .expect("client should be created when probe succeeds"); + } + + #[tokio::test] + async fn test_try_from_oss_provider_err_when_server_missing() { + if std::env::var(codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { + tracing::info!( + "{} set; skipping test_try_from_oss_provider_err_when_server_missing", + codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR + ); + return; + } + + let server = wiremock::MockServer::start().await; + // Point oss provider at our mock server but do NOT set up a handler + // for /v1/models so the request returns a non‑success status. + unsafe { std::env::set_var("CODEX_OSS_BASE_URL", format!("{}/v1", server.uri())) }; + + let err = OllamaClient::try_from_oss_provider() + .await + .err() + .expect("expected error"); + let msg = err.to_string(); + assert!( + msg.contains("No running Ollama server detected."), + "msg = {msg}" + ); + } +} diff --git a/codex-rs/ollama/src/lib.rs b/codex-rs/ollama/src/lib.rs new file mode 100644 index 0000000000..d6f1e04d1f --- /dev/null +++ b/codex-rs/ollama/src/lib.rs @@ -0,0 +1,52 @@ +mod client; +mod parser; +mod pull; +mod url; + +pub use client::OllamaClient; +pub use pull::CliProgressReporter; +pub use pull::PullEvent; +pub use pull::PullProgressReporter; +pub use pull::TuiProgressReporter; + +/// Default OSS model to use when `--oss` is passed without an explicit `-m`. +pub const DEFAULT_OSS_MODEL: &str = "gpt-oss:20b"; + +/// Prepare the local OSS environment when `--oss` is selected. +/// +/// - Ensures a local Ollama server is reachable. +/// - Selects the final model name (CLI override or default). +/// - Checks if the model exists locally and pulls it if missing. +/// +/// Returns the final model name that should be used by the caller. +pub async fn ensure_oss_ready(cli_model: Option) -> std::io::Result { + // Only download when the requested model is the default OSS model (or when -m is not provided). + let should_download = cli_model + .as_deref() + .map(|name| name == DEFAULT_OSS_MODEL) + .unwrap_or(true); + let model = cli_model.unwrap_or_else(|| DEFAULT_OSS_MODEL.to_string()); + + // Verify local Ollama is reachable. + let ollama_client = crate::OllamaClient::try_from_oss_provider().await?; + + if should_download { + // If the model is not present locally, pull it. + match ollama_client.fetch_models().await { + Ok(models) => { + if !models.iter().any(|m| m == &model) { + let mut reporter = crate::CliProgressReporter::new(); + ollama_client + .pull_with_reporter(&model, &mut reporter) + .await?; + } + } + Err(err) => { + // Not fatal; higher layers may still proceed and surface errors later. + tracing::warn!("Failed to query local models from Ollama: {}.", err); + } + } + } + + Ok(model) +} diff --git a/codex-rs/ollama/src/parser.rs b/codex-rs/ollama/src/parser.rs new file mode 100644 index 0000000000..b3ed2ca8c3 --- /dev/null +++ b/codex-rs/ollama/src/parser.rs @@ -0,0 +1,82 @@ +use serde_json::Value as JsonValue; + +use crate::pull::PullEvent; + +// Convert a single JSON object representing a pull update into one or more events. +pub(crate) fn pull_events_from_value(value: &JsonValue) -> Vec { + let mut events = Vec::new(); + if let Some(status) = value.get("status").and_then(|s| s.as_str()) { + events.push(PullEvent::Status(status.to_string())); + if status == "success" { + events.push(PullEvent::Success); + } + } + let digest = value + .get("digest") + .and_then(|d| d.as_str()) + .unwrap_or("") + .to_string(); + let total = value.get("total").and_then(|t| t.as_u64()); + let completed = value.get("completed").and_then(|t| t.as_u64()); + if total.is_some() || completed.is_some() { + events.push(PullEvent::ChunkProgress { + digest, + total, + completed, + }); + } + events +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pull_events_decoder_status_and_success() { + let v: JsonValue = serde_json::json!({"status":"verifying"}); + let events = pull_events_from_value(&v); + assert!(matches!(events.as_slice(), [PullEvent::Status(s)] if s == "verifying")); + + let v2: JsonValue = serde_json::json!({"status":"success"}); + let events2 = pull_events_from_value(&v2); + assert_eq!(events2.len(), 2); + assert!(matches!(events2[0], PullEvent::Status(ref s) if s == "success")); + assert!(matches!(events2[1], PullEvent::Success)); + } + + #[test] + fn test_pull_events_decoder_progress() { + let v: JsonValue = serde_json::json!({"digest":"sha256:abc","total":100}); + let events = pull_events_from_value(&v); + assert_eq!(events.len(), 1); + match &events[0] { + PullEvent::ChunkProgress { + digest, + total, + completed, + } => { + assert_eq!(digest, "sha256:abc"); + assert_eq!(*total, Some(100)); + assert_eq!(*completed, None); + } + _ => panic!("expected ChunkProgress"), + } + + let v2: JsonValue = serde_json::json!({"digest":"sha256:def","completed":42}); + let events2 = pull_events_from_value(&v2); + assert_eq!(events2.len(), 1); + match &events2[0] { + PullEvent::ChunkProgress { + digest, + total, + completed, + } => { + assert_eq!(digest, "sha256:def"); + assert_eq!(*total, None); + assert_eq!(*completed, Some(42)); + } + _ => panic!("expected ChunkProgress"), + } + } +} diff --git a/codex-rs/ollama/src/pull.rs b/codex-rs/ollama/src/pull.rs new file mode 100644 index 0000000000..0dd35cd786 --- /dev/null +++ b/codex-rs/ollama/src/pull.rs @@ -0,0 +1,147 @@ +use std::collections::HashMap; +use std::io; +use std::io::Write; + +/// Events emitted while pulling a model from Ollama. +#[derive(Debug, Clone)] +pub enum PullEvent { + /// A human-readable status message (e.g., "verifying", "writing"). + Status(String), + /// Byte-level progress update for a specific layer digest. + ChunkProgress { + digest: String, + total: Option, + completed: Option, + }, + /// The pull finished successfully. + Success, + + /// Error event with a message. + Error(String), +} + +/// A simple observer for pull progress events. Implementations decide how to +/// render progress (CLI, TUI, logs, ...). +pub trait PullProgressReporter { + fn on_event(&mut self, event: &PullEvent) -> io::Result<()>; +} + +/// A minimal CLI reporter that writes inline progress to stderr. +pub struct CliProgressReporter { + printed_header: bool, + last_line_len: usize, + last_completed_sum: u64, + last_instant: std::time::Instant, + totals_by_digest: HashMap, +} + +impl Default for CliProgressReporter { + fn default() -> Self { + Self::new() + } +} + +impl CliProgressReporter { + pub fn new() -> Self { + Self { + printed_header: false, + last_line_len: 0, + last_completed_sum: 0, + last_instant: std::time::Instant::now(), + totals_by_digest: HashMap::new(), + } + } +} + +impl PullProgressReporter for CliProgressReporter { + fn on_event(&mut self, event: &PullEvent) -> io::Result<()> { + let mut out = std::io::stderr(); + match event { + PullEvent::Status(status) => { + // Avoid noisy manifest messages; otherwise show status inline. + if status.eq_ignore_ascii_case("pulling manifest") { + return Ok(()); + } + let pad = self.last_line_len.saturating_sub(status.len()); + let line = format!("\r{status}{}", " ".repeat(pad)); + self.last_line_len = status.len(); + out.write_all(line.as_bytes())?; + out.flush() + } + PullEvent::ChunkProgress { + digest, + total, + completed, + } => { + if let Some(t) = *total { + self.totals_by_digest + .entry(digest.clone()) + .or_insert((0, 0)) + .0 = t; + } + if let Some(c) = *completed { + self.totals_by_digest + .entry(digest.clone()) + .or_insert((0, 0)) + .1 = c; + } + + let (sum_total, sum_completed) = self + .totals_by_digest + .values() + .fold((0u64, 0u64), |acc, (t, c)| (acc.0 + *t, acc.1 + *c)); + if sum_total > 0 { + if !self.printed_header { + let gb = (sum_total as f64) / (1024.0 * 1024.0 * 1024.0); + let header = format!("Downloading model: total {gb:.2} GB\n"); + out.write_all(b"\r\x1b[2K")?; + out.write_all(header.as_bytes())?; + self.printed_header = true; + } + let now = std::time::Instant::now(); + let dt = now + .duration_since(self.last_instant) + .as_secs_f64() + .max(0.001); + let dbytes = sum_completed.saturating_sub(self.last_completed_sum) as f64; + let speed_mb_s = dbytes / (1024.0 * 1024.0) / dt; + self.last_completed_sum = sum_completed; + self.last_instant = now; + + let done_gb = (sum_completed as f64) / (1024.0 * 1024.0 * 1024.0); + let total_gb = (sum_total as f64) / (1024.0 * 1024.0 * 1024.0); + let pct = (sum_completed as f64) * 100.0 / (sum_total as f64); + let text = + format!("{done_gb:.2}/{total_gb:.2} GB ({pct:.1}%) {speed_mb_s:.1} MB/s"); + let pad = self.last_line_len.saturating_sub(text.len()); + let line = format!("\r{text}{}", " ".repeat(pad)); + self.last_line_len = text.len(); + out.write_all(line.as_bytes())?; + out.flush() + } else { + Ok(()) + } + } + PullEvent::Error(_) => { + // This will be handled by the caller, so we don't do anything + // here or the error will be printed twice. + Ok(()) + } + PullEvent::Success => { + out.write_all(b"\n")?; + out.flush() + } + } + } +} + +/// For now the TUI reporter delegates to the CLI reporter. This keeps UI and +/// CLI behavior aligned until a dedicated TUI integration is implemented. +#[derive(Default)] +pub struct TuiProgressReporter(CliProgressReporter); + +impl PullProgressReporter for TuiProgressReporter { + fn on_event(&mut self, event: &PullEvent) -> io::Result<()> { + self.0.on_event(event) + } +} diff --git a/codex-rs/ollama/src/url.rs b/codex-rs/ollama/src/url.rs new file mode 100644 index 0000000000..7c143ce426 --- /dev/null +++ b/codex-rs/ollama/src/url.rs @@ -0,0 +1,39 @@ +/// Identify whether a base_url points at an OpenAI-compatible root (".../v1"). +pub(crate) fn is_openai_compatible_base_url(base_url: &str) -> bool { + base_url.trim_end_matches('/').ends_with("/v1") +} + +/// Convert a provider base_url into the native Ollama host root. +/// For example, "http://localhost:11434/v1" -> "http://localhost:11434". +pub fn base_url_to_host_root(base_url: &str) -> String { + let trimmed = base_url.trim_end_matches('/'); + if trimmed.ends_with("/v1") { + trimmed + .trim_end_matches("/v1") + .trim_end_matches('/') + .to_string() + } else { + trimmed.to_string() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_base_url_to_host_root() { + assert_eq!( + base_url_to_host_root("http://localhost:11434/v1"), + "http://localhost:11434" + ); + assert_eq!( + base_url_to_host_root("http://localhost:11434"), + "http://localhost:11434" + ); + assert_eq!( + base_url_to_host_root("http://localhost:11434/"), + "http://localhost:11434" + ); + } +} diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 60af056a2d..49d843f046 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -33,6 +33,7 @@ codex-common = { path = "../common", features = [ codex-core = { path = "../core" } codex-file-search = { path = "../file-search" } codex-login = { path = "../login" } +codex-ollama = { path = "../ollama" } color-eyre = "0.6.3" crossterm = { version = "0.28.1", features = ["bracketed-paste"] } image = { version = "^0.25.6", default-features = false, features = ["jpeg"] } diff --git a/codex-rs/tui/src/cli.rs b/codex-rs/tui/src/cli.rs index cb1b725a64..85dffbebb3 100644 --- a/codex-rs/tui/src/cli.rs +++ b/codex-rs/tui/src/cli.rs @@ -17,6 +17,12 @@ pub struct Cli { #[arg(long, short = 'm')] pub model: Option, + /// Convenience flag to select the local open source model provider. + /// Equivalent to -c model_provider=oss; verifies a local Ollama server is + /// running. + #[arg(long = "oss", default_value_t = false)] + pub oss: bool, + /// Configuration profile from config.toml to specify default options. #[arg(long = "profile", short = 'p')] pub config_profile: Option, diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index c619ce8ff0..0b833b13ae 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -3,6 +3,7 @@ // alternate‑screen mode starts; that file opts‑out locally via `allow`. #![deny(clippy::print_stdout, clippy::print_stderr)] use app::App; +use codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID; use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_core::config_types::SandboxMode; @@ -70,18 +71,35 @@ pub async fn run_main( ) }; + let model_provider_override = if cli.oss { + Some(BUILT_IN_OSS_MODEL_PROVIDER_ID.to_owned()) + } else { + None + }; let config = { // Load configuration and support CLI overrides. let overrides = ConfigOverrides { - model: cli.model.clone(), + // When using `--oss`, let the bootstrapper pick the model + // (defaulting to gpt-oss:20b) and ensure it is present locally. + model: if cli.oss { + Some( + codex_ollama::ensure_oss_ready(cli.model.clone()) + .await + .map_err(|e| std::io::Error::other(format!("OSS setup failed: {e}")))?, + ) + } else { + cli.model.clone() + }, approval_policy, sandbox_mode, cwd: cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p)), - model_provider: None, + model_provider: model_provider_override, config_profile: cli.config_profile.clone(), codex_linux_sandbox_exe, base_instructions: None, include_plan_tool: Some(true), + default_disable_response_storage: cli.oss.then_some(true), + default_show_raw_agent_reasoning: cli.oss.then_some(true), }; // Parse `-c` overrides from the CLI. let cli_kv_overrides = match cli.config_overrides.parse_overrides() { From 9f91b3da243fe05ab63b21db7f75848a7cc4a398 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 5 Aug 2025 11:39:30 -0700 Subject: [PATCH 0020/1309] fix: correct spelling error that sneaked through (#1855) I ended up force-pushing https://github.com/openai/codex/pull/1848 because CI jobs were not being triggered after updating the PR on GitHub, so this spelling error sneaked through. --- codex-rs/ollama/src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/ollama/src/client.rs b/codex-rs/ollama/src/client.rs index 8a15039fad..f86271dc5e 100644 --- a/codex-rs/ollama/src/client.rs +++ b/codex-rs/ollama/src/client.rs @@ -192,7 +192,7 @@ impl OllamaClient { return Ok(()); } PullEvent::Error(err) => { - // Emperically, ollama returns a 200 OK response even when + // Empirically, ollama returns a 200 OK response even when // the output stream includes an error message. Verify with: // // `curl -i http://localhost:11434/api/pull -d '{ "model": "foobarbaz" }'` From bd24bc320ecd18d214cb194af88555fbf6fbd264 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 5 Aug 2025 11:44:04 -0700 Subject: [PATCH 0021/1309] fix: clean out some ASCII (#1856) Similar to https://github.com/openai/codex/pull/1855, this got through. Fixed by running: ``` ./scripts/asciicheck.py README.md ``` --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dd5e466252..b323d8978d 100644 --- a/README.md +++ b/README.md @@ -189,11 +189,11 @@ they'll be committed to your working directory. ## Using Open Source Models -Codex can run fully locally against an OpenAI‑compatible OSS host (like Ollama) using the `--oss` flag: +Codex can run fully locally against an OpenAI-compatible OSS host (like Ollama) using the `--oss` flag: - Interactive UI: - codex --oss -- Non‑interactive (programmatic) mode: +- Non-interactive (programmatic) mode: - echo "Refactor utils" | codex exec --oss Model selection when using `--oss`: @@ -212,7 +212,7 @@ Point Codex at your own OSS host: - or CODEX_OSS_PORT (when the host is localhost): - CODEX_OSS_PORT=11434 codex --oss -Advanced: you can persist this in your config instead of environment variables by overriding the built‑in `oss` provider in `~/.codex/config.toml`: +Advanced: you can persist this in your config instead of environment variables by overriding the built-in `oss` provider in `~/.codex/config.toml`: ```toml [model_providers.oss] From 0c5fa271bcce88748eb87210588ba3ef88c559dc Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 5 Aug 2025 11:48:28 -0700 Subject: [PATCH 0022/1309] fix: README ToC did not match contents (#1857) Similar to https://github.com/openai/codex/pull/1855, this got through. Fixed by running: ``` python3 scripts/readme_toc.py --fix README.md ``` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b323d8978d..31e87f055a 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ This is the home of the **Codex CLI**, which is a coding agent from OpenAI that - [Quickstart](#quickstart) - [OpenAI API Users](#openai-api-users) - [OpenAI Plus/Pro Users](#openai-pluspro-users) - - [Using OpenAI Open Source Models](#using-open-source-models) +- [Using Open Source Models](#using-open-source-models) - [Why Codex?](#why-codex) - [Security model & permissions](#security-model--permissions) - [Platform sandboxing details](#platform-sandboxing-details) From d365cae0771855d2ac2bddc90ef04b60ca872e7e Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 5 Aug 2025 13:55:32 -0700 Subject: [PATCH 0023/1309] fix: when using `--oss`, ensure correct configuration is threaded through correctly (#1859) This PR started as an investigation with the goal of eliminating the use of `unsafe { std::env::set_var() }` in `ollama/src/client.rs`, as setting environment variables in a multithreaded context is indeed unsafe and these tests were observed to be flaky, as a result. Though as I dug deeper into the issue, I discovered that the logic for instantiating `OllamaClient` under test scenarios was not quite right. In this PR, I aimed to: - share more code between the two creation codepaths, `try_from_oss_provider()` and `try_from_provider_with_base_url()` - use the values from `Config` when setting up Ollama, as we have various mechanisms for overriding config values, so we should be sure that we are always using the ultimate `Config` for things such as the `ModelProviderInfo` associated with the `oss` id Once this was in place, `OllamaClient::try_from_provider_with_base_url()` could be used in unit tests for `OllamaClient` so it was possible to create a properly configured client without having to set environment variables. --- codex-rs/core/src/lib.rs | 1 + codex-rs/core/src/model_provider_info.rs | 73 +++++----- codex-rs/exec/src/lib.rs | 30 ++-- codex-rs/ollama/src/client.rs | 177 +++++++++++------------ codex-rs/ollama/src/lib.rs | 42 +++--- codex-rs/tui/src/lib.rs | 31 ++-- 6 files changed, 176 insertions(+), 178 deletions(-) diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 965cb77bf1..d072613e10 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -32,6 +32,7 @@ pub use model_provider_info::BUILT_IN_OSS_MODEL_PROVIDER_ID; pub use model_provider_info::ModelProviderInfo; pub use model_provider_info::WireApi; pub use model_provider_info::built_in_model_providers; +pub use model_provider_info::create_oss_provider_with_base_url; pub mod model_family; mod models; mod openai_model_info; diff --git a/codex-rs/core/src/model_provider_info.rs b/codex-rs/core/src/model_provider_info.rs index 595f05ef75..db369df3b7 100644 --- a/codex-rs/core/src/model_provider_info.rs +++ b/codex-rs/core/src/model_provider_info.rs @@ -234,23 +234,6 @@ pub const BUILT_IN_OSS_MODEL_PROVIDER_ID: &str = "oss"; pub fn built_in_model_providers() -> HashMap { use ModelProviderInfo as P; - // These CODEX_OSS_ environment variables are experimental: we may - // switch to reading values from config.toml instead. - let codex_oss_base_url = match std::env::var("CODEX_OSS_BASE_URL") - .ok() - .filter(|v| !v.trim().is_empty()) - { - Some(url) => url, - None => format!( - "http://localhost:{port}/v1", - port = std::env::var("CODEX_OSS_PORT") - .ok() - .filter(|v| !v.trim().is_empty()) - .and_then(|v| v.parse::().ok()) - .unwrap_or(DEFAULT_OLLAMA_PORT) - ), - }; - // We do not want to be in the business of adjucating which third-party // providers are bundled with Codex CLI, so we only include the OpenAI and // open source ("oss") providers by default. Users are encouraged to add to @@ -295,29 +278,51 @@ pub fn built_in_model_providers() -> HashMap { requires_auth: true, }, ), - ( - BUILT_IN_OSS_MODEL_PROVIDER_ID, - P { - name: "Open Source".into(), - base_url: Some(codex_oss_base_url), - env_key: None, - env_key_instructions: None, - wire_api: WireApi::Chat, - query_params: None, - http_headers: None, - env_http_headers: None, - request_max_retries: None, - stream_max_retries: None, - stream_idle_timeout_ms: None, - requires_auth: false, - }, - ), + (BUILT_IN_OSS_MODEL_PROVIDER_ID, create_oss_provider()), ] .into_iter() .map(|(k, v)| (k.to_string(), v)) .collect() } +pub fn create_oss_provider() -> ModelProviderInfo { + // These CODEX_OSS_ environment variables are experimental: we may + // switch to reading values from config.toml instead. + let codex_oss_base_url = match std::env::var("CODEX_OSS_BASE_URL") + .ok() + .filter(|v| !v.trim().is_empty()) + { + Some(url) => url, + None => format!( + "http://localhost:{port}/v1", + port = std::env::var("CODEX_OSS_PORT") + .ok() + .filter(|v| !v.trim().is_empty()) + .and_then(|v| v.parse::().ok()) + .unwrap_or(DEFAULT_OLLAMA_PORT) + ), + }; + + create_oss_provider_with_base_url(&codex_oss_base_url) +} + +pub fn create_oss_provider_with_base_url(base_url: &str) -> ModelProviderInfo { + ModelProviderInfo { + name: "gpt-oss".into(), + base_url: Some(base_url.into()), + env_key: None, + env_key_instructions: None, + wire_api: WireApi::Chat, + query_params: None, + http_headers: None, + env_http_headers: None, + request_max_retries: None, + stream_max_retries: None, + stream_idle_timeout_ms: None, + requires_auth: false, + } +} + #[cfg(test)] mod tests { #![allow(clippy::unwrap_used)] diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index c1af4f5b45..a0360182b4 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -22,6 +22,7 @@ use codex_core::protocol::InputItem; use codex_core::protocol::Op; use codex_core::protocol::TaskCompleteEvent; use codex_core::util::is_inside_git_repo; +use codex_ollama::DEFAULT_OSS_MODEL; use event_processor_with_human_output::EventProcessorWithHumanOutput; use event_processor_with_json_output::EventProcessorWithJsonOutput; use tracing::debug; @@ -35,7 +36,7 @@ use crate::event_processor::EventProcessor; pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> anyhow::Result<()> { let Cli { images, - model, + model: model_cli_arg, oss, config_profile, full_auto, @@ -119,19 +120,18 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any // When using `--oss`, let the bootstrapper pick the model (defaulting to // gpt-oss:20b) and ensure it is present locally. Also, force the built‑in // `oss` model provider. - let model_provider_override = if oss { - Some(BUILT_IN_OSS_MODEL_PROVIDER_ID.to_owned()) + let model = if let Some(model) = model_cli_arg { + Some(model) + } else if oss { + Some(DEFAULT_OSS_MODEL.to_owned()) } else { - None + None // No model specified, will use the default. }; - let model = if oss { - Some( - codex_ollama::ensure_oss_ready(model.clone()) - .await - .map_err(|e| anyhow::anyhow!("OSS setup failed: {e}"))?, - ) + + let model_provider = if oss { + Some(BUILT_IN_OSS_MODEL_PROVIDER_ID.to_string()) } else { - model + None // No specific model provider override. }; // Load configuration and determine approval policy @@ -143,7 +143,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any approval_policy: Some(AskForApproval::Never), sandbox_mode, cwd: cwd.map(|p| p.canonicalize().unwrap_or(p)), - model_provider: model_provider_override, + model_provider, codex_linux_sandbox_exe, base_instructions: None, include_plan_tool: None, @@ -170,6 +170,12 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any )) }; + if oss { + codex_ollama::ensure_oss_ready(&config) + .await + .map_err(|e| anyhow::anyhow!("OSS setup failed: {e}"))?; + } + // Print the effective configuration and prompt so users can see what Codex // is using. event_processor.print_config_summary(&config, &prompt); diff --git a/codex-rs/ollama/src/client.rs b/codex-rs/ollama/src/client.rs index f86271dc5e..6f4621135c 100644 --- a/codex-rs/ollama/src/client.rs +++ b/codex-rs/ollama/src/client.rs @@ -5,13 +5,17 @@ use serde_json::Value as JsonValue; use std::collections::VecDeque; use std::io; -use codex_core::WireApi; - use crate::parser::pull_events_from_value; use crate::pull::PullEvent; use crate::pull::PullProgressReporter; use crate::url::base_url_to_host_root; use crate::url::is_openai_compatible_base_url; +use codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID; +use codex_core::ModelProviderInfo; +use codex_core::WireApi; +use codex_core::config::Config; + +const OLLAMA_CONNECTION_ERROR: &str = "No running Ollama server detected. Start it with: `ollama serve` (after installing). Install instructions: https://github.com/ollama/ollama?tab=readme-ov-file#ollama"; /// Client for interacting with a local Ollama instance. pub struct OllamaClient { @@ -21,74 +25,77 @@ pub struct OllamaClient { } impl OllamaClient { - pub fn from_oss_provider() -> Self { + /// Construct a client for the built‑in open‑source ("oss") model provider + /// and verify that a local Ollama server is reachable. If no server is + /// detected, returns an error with helpful installation/run instructions. + pub async fn try_from_oss_provider(config: &Config) -> io::Result { + // Note that we must look up the provider from the Config to ensure that + // any overrides the user has in their config.toml are taken into + // account. + let provider = config + .model_providers + .get(BUILT_IN_OSS_MODEL_PROVIDER_ID) + .ok_or_else(|| { + io::Error::new( + io::ErrorKind::NotFound, + format!("Built-in provider {BUILT_IN_OSS_MODEL_PROVIDER_ID} not found",), + ) + })?; + + Self::try_from_provider(provider).await + } + + #[cfg(test)] + async fn try_from_provider_with_base_url(base_url: &str) -> io::Result { + let provider = codex_core::create_oss_provider_with_base_url(base_url); + Self::try_from_provider(&provider).await + } + + /// Build a client from a provider definition and verify the server is reachable. + async fn try_from_provider(provider: &ModelProviderInfo) -> io::Result { #![allow(clippy::expect_used)] - // Use the built-in OSS provider's base URL. - let built_in_model_providers = codex_core::built_in_model_providers(); - let provider = built_in_model_providers - .get(codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID) - .expect("oss provider must exist"); let base_url = provider .base_url .as_ref() .expect("oss provider must have a base_url"); - Self::from_provider(base_url, provider.wire_api) - } - - /// Construct a client for the built‑in open‑source ("oss") model provider - /// and verify that a local Ollama server is reachable. If no server is - /// detected, returns an error with helpful installation/run instructions. - pub async fn try_from_oss_provider() -> io::Result { - let client = Self::from_oss_provider(); - if client.probe_server().await? { - Ok(client) - } else { - Err(io::Error::other( - "No running Ollama server detected. Start it with: `ollama serve` (after installing). Install instructions: https://github.com/ollama/ollama?tab=readme-ov-file#ollama", - )) - } - } - - /// Build a client from a provider definition. Falls back to the default - /// local URL if no base_url is configured. - fn from_provider(base_url: &str, wire_api: WireApi) -> Self { let uses_openai_compat = is_openai_compatible_base_url(base_url) - || matches!(wire_api, WireApi::Chat) && is_openai_compatible_base_url(base_url); + || matches!(provider.wire_api, WireApi::Chat) + && is_openai_compatible_base_url(base_url); let host_root = base_url_to_host_root(base_url); let client = reqwest::Client::builder() .connect_timeout(std::time::Duration::from_secs(5)) .build() .unwrap_or_else(|_| reqwest::Client::new()); - Self { + let client = Self { client, host_root, uses_openai_compat, - } - } - - /// Low-level constructor given a raw host root, e.g. "http://localhost:11434". - #[cfg(test)] - fn from_host_root(host_root: impl Into) -> Self { - let client = reqwest::Client::builder() - .connect_timeout(std::time::Duration::from_secs(5)) - .build() - .unwrap_or_else(|_| reqwest::Client::new()); - Self { - client, - host_root: host_root.into(), - uses_openai_compat: false, - } + }; + client.probe_server().await?; + Ok(client) } /// Probe whether the server is reachable by hitting the appropriate health endpoint. - pub async fn probe_server(&self) -> io::Result { + async fn probe_server(&self) -> io::Result<()> { let url = if self.uses_openai_compat { format!("{}/v1/models", self.host_root.trim_end_matches('/')) } else { format!("{}/api/tags", self.host_root.trim_end_matches('/')) }; - let resp = self.client.get(url).send().await; - Ok(matches!(resp, Ok(r) if r.status().is_success())) + let resp = self.client.get(url).send().await.map_err(|err| { + tracing::warn!("Failed to connect to Ollama server: {err:?}"); + io::Error::other(OLLAMA_CONNECTION_ERROR) + })?; + if resp.status().is_success() { + Ok(()) + } else { + tracing::warn!( + "Failed to probe server at {}: HTTP {}", + self.host_root, + resp.status() + ); + Err(io::Error::other(OLLAMA_CONNECTION_ERROR)) + } } /// Return the list of model names known to the local Ollama instance. @@ -210,6 +217,20 @@ impl OllamaClient { "Pull stream ended unexpectedly without success.", )) } + + /// Low-level constructor given a raw host root, e.g. "http://localhost:11434". + #[cfg(test)] + fn from_host_root(host_root: impl Into) -> Self { + let client = reqwest::Client::builder() + .connect_timeout(std::time::Duration::from_secs(5)) + .build() + .unwrap_or_else(|_| reqwest::Client::new()); + Self { + client, + host_root: host_root.into(), + uses_openai_compat: false, + } + } } #[cfg(test)] @@ -217,34 +238,6 @@ mod tests { #![allow(clippy::expect_used, clippy::unwrap_used)] use super::*; - /// Simple RAII guard to set an environment variable for the duration of a test - /// and restore the previous value (or remove it) on drop to avoid cross-test - /// interference. - struct EnvVarGuard { - key: String, - prev: Option, - } - impl EnvVarGuard { - fn set(key: &str, value: String) -> Self { - let prev = std::env::var(key).ok(); - // set_var is safe but we mirror existing tests that use an unsafe block - // to silence edition lints around global mutation during tests. - unsafe { std::env::set_var(key, value) }; - Self { - key: key.to_string(), - prev, - } - } - } - impl Drop for EnvVarGuard { - fn drop(&mut self) { - match &self.prev { - Some(v) => unsafe { std::env::set_var(&self.key, v) }, - None => unsafe { std::env::remove_var(&self.key) }, - } - } - } - // Happy-path tests using a mock HTTP server; skip if sandbox network is disabled. #[tokio::test] async fn test_fetch_models_happy_path() { @@ -296,7 +289,7 @@ mod tests { .mount(&server) .await; let native = OllamaClient::from_host_root(server.uri()); - assert!(native.probe_server().await.expect("probe native")); + native.probe_server().await.expect("probe native"); // OpenAI compatibility endpoint wiremock::Mock::given(wiremock::matchers::method("GET")) @@ -304,11 +297,14 @@ mod tests { .respond_with(wiremock::ResponseTemplate::new(200)) .mount(&server) .await; - // Ensure the built-in OSS provider points at our mock server for this test - // to avoid depending on any globally configured environment from other tests. - let _guard = EnvVarGuard::set("CODEX_OSS_BASE_URL", format!("{}/v1", server.uri())); - let ollama_client = OllamaClient::from_oss_provider(); - assert!(ollama_client.probe_server().await.expect("probe compat")); + let ollama_client = + OllamaClient::try_from_provider_with_base_url(&format!("{}/v1", server.uri())) + .await + .expect("probe OpenAI compat"); + ollama_client + .probe_server() + .await + .expect("probe OpenAI compat"); } #[tokio::test] @@ -322,9 +318,6 @@ mod tests { } let server = wiremock::MockServer::start().await; - // Configure built‑in `oss` provider to point at this mock server. - // set_var is unsafe on Rust 2024 edition; use unsafe block in tests. - let _guard = EnvVarGuard::set("CODEX_OSS_BASE_URL", format!("{}/v1", server.uri())); // OpenAI‑compat models endpoint responds OK. wiremock::Mock::given(wiremock::matchers::method("GET")) @@ -333,7 +326,7 @@ mod tests { .mount(&server) .await; - let _client = OllamaClient::try_from_oss_provider() + OllamaClient::try_from_provider_with_base_url(&format!("{}/v1", server.uri())) .await .expect("client should be created when probe succeeds"); } @@ -349,18 +342,10 @@ mod tests { } let server = wiremock::MockServer::start().await; - // Point oss provider at our mock server but do NOT set up a handler - // for /v1/models so the request returns a non‑success status. - unsafe { std::env::set_var("CODEX_OSS_BASE_URL", format!("{}/v1", server.uri())) }; - - let err = OllamaClient::try_from_oss_provider() + let err = OllamaClient::try_from_provider_with_base_url(&format!("{}/v1", server.uri())) .await .err() .expect("expected error"); - let msg = err.to_string(); - assert!( - msg.contains("No running Ollama server detected."), - "msg = {msg}" - ); + assert_eq!(OLLAMA_CONNECTION_ERROR, err.to_string()); } } diff --git a/codex-rs/ollama/src/lib.rs b/codex-rs/ollama/src/lib.rs index d6f1e04d1f..0ebf1662ac 100644 --- a/codex-rs/ollama/src/lib.rs +++ b/codex-rs/ollama/src/lib.rs @@ -4,6 +4,7 @@ mod pull; mod url; pub use client::OllamaClient; +use codex_core::config::Config; pub use pull::CliProgressReporter; pub use pull::PullEvent; pub use pull::PullProgressReporter; @@ -15,38 +16,29 @@ pub const DEFAULT_OSS_MODEL: &str = "gpt-oss:20b"; /// Prepare the local OSS environment when `--oss` is selected. /// /// - Ensures a local Ollama server is reachable. -/// - Selects the final model name (CLI override or default). /// - Checks if the model exists locally and pulls it if missing. -/// -/// Returns the final model name that should be used by the caller. -pub async fn ensure_oss_ready(cli_model: Option) -> std::io::Result { +pub async fn ensure_oss_ready(config: &Config) -> std::io::Result<()> { // Only download when the requested model is the default OSS model (or when -m is not provided). - let should_download = cli_model - .as_deref() - .map(|name| name == DEFAULT_OSS_MODEL) - .unwrap_or(true); - let model = cli_model.unwrap_or_else(|| DEFAULT_OSS_MODEL.to_string()); + let model = config.model.as_ref(); // Verify local Ollama is reachable. - let ollama_client = crate::OllamaClient::try_from_oss_provider().await?; + let ollama_client = crate::OllamaClient::try_from_oss_provider(config).await?; - if should_download { - // If the model is not present locally, pull it. - match ollama_client.fetch_models().await { - Ok(models) => { - if !models.iter().any(|m| m == &model) { - let mut reporter = crate::CliProgressReporter::new(); - ollama_client - .pull_with_reporter(&model, &mut reporter) - .await?; - } - } - Err(err) => { - // Not fatal; higher layers may still proceed and surface errors later. - tracing::warn!("Failed to query local models from Ollama: {}.", err); + // If the model is not present locally, pull it. + match ollama_client.fetch_models().await { + Ok(models) => { + if !models.iter().any(|m| m == model) { + let mut reporter = crate::CliProgressReporter::new(); + ollama_client + .pull_with_reporter(model, &mut reporter) + .await?; } } + Err(err) => { + // Not fatal; higher layers may still proceed and surface errors later. + tracing::warn!("Failed to query local models from Ollama: {}.", err); + } } - Ok(model) + Ok(()) } diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 0b833b13ae..bab728e124 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -10,6 +10,7 @@ use codex_core::config_types::SandboxMode; use codex_core::protocol::AskForApproval; use codex_core::util::is_inside_git_repo; use codex_login::load_auth; +use codex_ollama::DEFAULT_OSS_MODEL; use log_layer::TuiLogLayer; use std::fs::OpenOptions; use std::io::Write; @@ -71,25 +72,27 @@ pub async fn run_main( ) }; + // When using `--oss`, let the bootstrapper pick the model (defaulting to + // gpt-oss:20b) and ensure it is present locally. Also, force the built‑in + // `oss` model provider. + let model = if let Some(model) = &cli.model { + Some(model.clone()) + } else if cli.oss { + Some(DEFAULT_OSS_MODEL.to_owned()) + } else { + None // No model specified, will use the default. + }; + let model_provider_override = if cli.oss { Some(BUILT_IN_OSS_MODEL_PROVIDER_ID.to_owned()) } else { None }; + let config = { // Load configuration and support CLI overrides. let overrides = ConfigOverrides { - // When using `--oss`, let the bootstrapper pick the model - // (defaulting to gpt-oss:20b) and ensure it is present locally. - model: if cli.oss { - Some( - codex_ollama::ensure_oss_ready(cli.model.clone()) - .await - .map_err(|e| std::io::Error::other(format!("OSS setup failed: {e}")))?, - ) - } else { - cli.model.clone() - }, + model, approval_policy, sandbox_mode, cwd: cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p)), @@ -154,6 +157,12 @@ pub async fn run_main( .with_target(false) .with_filter(env_filter()); + if cli.oss { + codex_ollama::ensure_oss_ready(&config) + .await + .map_err(|e| std::io::Error::other(format!("OSS setup failed: {e}")))?; + } + // Channel that carries formatted log lines to the UI. let (log_tx, log_rx) = tokio::sync::mpsc::unbounded_channel::(); let tui_layer = TuiLogLayer::new(log_tx.clone(), 120).with_filter(env_filter()); From 42bd73e150c887a3ac102409a38d124c2f26ba0a Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 5 Aug 2025 14:42:49 -0700 Subject: [PATCH 0024/1309] chore: remove unnecessary default_ prefix (#1854) This prefix is not inline with the other fields on the `ConfigOverrides` struct. --- codex-rs/core/src/config.rs | 12 ++++++------ codex-rs/exec/src/lib.rs | 4 ++-- codex-rs/mcp-server/src/codex_tool_config.rs | 4 ++-- .../src/tool_handlers/create_conversation.rs | 4 ++-- codex-rs/tui/src/lib.rs | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index e62fcc39e2..0b53df5ab7 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -385,8 +385,8 @@ pub struct ConfigOverrides { pub codex_linux_sandbox_exe: Option, pub base_instructions: Option, pub include_plan_tool: Option, - pub default_disable_response_storage: Option, - pub default_show_raw_agent_reasoning: Option, + pub disable_response_storage: Option, + pub show_raw_agent_reasoning: Option, } impl Config { @@ -410,8 +410,8 @@ impl Config { codex_linux_sandbox_exe, base_instructions, include_plan_tool, - default_disable_response_storage, - default_show_raw_agent_reasoning, + disable_response_storage, + show_raw_agent_reasoning, } = overrides; let config_profile = match config_profile_key.as_ref().or(cfg.profile.as_ref()) { @@ -529,7 +529,7 @@ impl Config { disable_response_storage: config_profile .disable_response_storage .or(cfg.disable_response_storage) - .or(default_disable_response_storage) + .or(disable_response_storage) .unwrap_or(false), notify: cfg.notify, user_instructions, @@ -546,7 +546,7 @@ impl Config { hide_agent_reasoning: cfg.hide_agent_reasoning.unwrap_or(false), show_raw_agent_reasoning: cfg .show_raw_agent_reasoning - .or(default_show_raw_agent_reasoning) + .or(show_raw_agent_reasoning) .unwrap_or(false), model_reasoning_effort: config_profile .model_reasoning_effort diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index a0360182b4..288b6177e5 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -147,8 +147,8 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any codex_linux_sandbox_exe, base_instructions: None, include_plan_tool: None, - default_disable_response_storage: oss.then_some(true), - default_show_raw_agent_reasoning: oss.then_some(true), + disable_response_storage: oss.then_some(true), + show_raw_agent_reasoning: oss.then_some(true), }; // Parse `-c` overrides. let cli_kv_overrides = match config_overrides.parse_overrides() { diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs index f1a502bbb3..899451a50d 100644 --- a/codex-rs/mcp-server/src/codex_tool_config.rs +++ b/codex-rs/mcp-server/src/codex_tool_config.rs @@ -158,8 +158,8 @@ impl CodexToolCallParam { codex_linux_sandbox_exe, base_instructions, include_plan_tool, - default_disable_response_storage: None, - default_show_raw_agent_reasoning: None, + disable_response_storage: None, + show_raw_agent_reasoning: None, }; let cli_overrides = cli_overrides diff --git a/codex-rs/mcp-server/src/tool_handlers/create_conversation.rs b/codex-rs/mcp-server/src/tool_handlers/create_conversation.rs index c1f4035663..559bf72905 100644 --- a/codex-rs/mcp-server/src/tool_handlers/create_conversation.rs +++ b/codex-rs/mcp-server/src/tool_handlers/create_conversation.rs @@ -59,8 +59,8 @@ pub(crate) async fn handle_create_conversation( codex_linux_sandbox_exe: None, base_instructions, include_plan_tool: None, - default_disable_response_storage: None, - default_show_raw_agent_reasoning: None, + disable_response_storage: None, + show_raw_agent_reasoning: None, }; let cfg: CodexConfig = match CodexConfig::load_with_cli_overrides(cli_overrides, overrides) { diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index bab728e124..50535e5967 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -101,8 +101,8 @@ pub async fn run_main( codex_linux_sandbox_exe, base_instructions: None, include_plan_tool: Some(true), - default_disable_response_storage: cli.oss.then_some(true), - default_show_raw_agent_reasoning: cli.oss.then_some(true), + disable_response_storage: cli.oss.then_some(true), + show_raw_agent_reasoning: cli.oss.then_some(true), }; // Parse `-c` overrides from the CLI. let cli_kv_overrides = match cli.config_overrides.parse_overrides() { From f6c8d1117cfe8b17de3c5f5d077126279be44d8f Mon Sep 17 00:00:00 2001 From: ae Date: Tue, 5 Aug 2025 15:50:06 -0700 Subject: [PATCH 0025/1309] [feat] make approval key matching case insensitive (#1862) --- codex-rs/tui/src/user_approval_widget.rs | 75 +++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/codex-rs/tui/src/user_approval_widget.rs b/codex-rs/tui/src/user_approval_widget.rs index 91febde208..70b355d794 100644 --- a/codex-rs/tui/src/user_approval_widget.rs +++ b/codex-rs/tui/src/user_approval_widget.rs @@ -47,6 +47,8 @@ pub(crate) enum ApprovalRequest { } /// Options displayed in the *select* mode. +/// +/// The `key` is matched case-insensitively. struct SelectOption { label: Line<'static>, description: &'static str, @@ -187,6 +189,16 @@ impl UserApprovalWidget<'_> { } } + /// Normalize a key for comparison. + /// - For `KeyCode::Char`, converts to lowercase for case-insensitive matching. + /// - Other key codes are returned unchanged. + fn normalize_keycode(code: KeyCode) -> KeyCode { + match code { + KeyCode::Char(c) => KeyCode::Char(c.to_ascii_lowercase()), + other => other, + } + } + /// Handle Ctrl-C pressed by the user while the modal is visible. /// Behaves like pressing Escape: abort the request and close the modal. pub(crate) fn on_ctrl_c(&mut self) { @@ -210,7 +222,12 @@ impl UserApprovalWidget<'_> { self.send_decision(ReviewDecision::Abort); } other => { - if let Some(opt) = self.select_options.iter().find(|opt| opt.key == other) { + let normalized = Self::normalize_keycode(other); + if let Some(opt) = self + .select_options + .iter() + .find(|opt| Self::normalize_keycode(opt.key) == normalized) + { self.send_decision(opt.decision); } } @@ -330,3 +347,59 @@ impl WidgetRef for &UserApprovalWidget<'_> { ); } } + +#[cfg(test)] +mod tests { + use super::*; + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + use std::path::PathBuf; + use std::sync::mpsc::channel; + + #[test] + fn lowercase_shortcut_is_accepted() { + let (tx_raw, rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let req = ApprovalRequest::Exec { + id: "1".to_string(), + command: vec!["echo".to_string()], + cwd: PathBuf::new(), + reason: None, + }; + let mut widget = UserApprovalWidget::new(req, tx); + widget.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE)); + assert!(widget.is_complete()); + let events: Vec = rx.try_iter().collect(); + assert!(events.iter().any(|e| matches!( + e, + AppEvent::CodexOp(Op::ExecApproval { + decision: ReviewDecision::Approved, + .. + }) + ))); + } + + #[test] + fn uppercase_shortcut_is_accepted() { + let (tx_raw, rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let req = ApprovalRequest::Exec { + id: "2".to_string(), + command: vec!["echo".to_string()], + cwd: PathBuf::new(), + reason: None, + }; + let mut widget = UserApprovalWidget::new(req, tx); + widget.handle_key_event(KeyEvent::new(KeyCode::Char('Y'), KeyModifiers::NONE)); + assert!(widget.is_complete()); + let events: Vec = rx.try_iter().collect(); + assert!(events.iter().any(|e| matches!( + e, + AppEvent::CodexOp(Op::ExecApproval { + decision: ReviewDecision::Approved, + .. + }) + ))); + } +} From ea7d3f27bdc1da61df979419515889f64f36c5ce Mon Sep 17 00:00:00 2001 From: Dylan Date: Tue, 5 Aug 2025 17:52:25 -0700 Subject: [PATCH 0026/1309] [core] Stop escalating timeouts (#1853) ## Summary Escalating out of sandbox is (almost always) not going to fix long-running commands timing out - therefore we should just pass the failure back to the model instead of asking the user to re-run a command that took a long time anyway. ## Testing - [x] Ran locally with a timeout and confirmed this worked as expected --- codex-rs/core/src/codex.rs | 14 ++++++++++++++ codex-rs/core/src/exec.rs | 39 +++++++++++++++++++------------------- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 0ce0c4ea2b..81ec3cab6e 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1957,6 +1957,20 @@ async fn handle_sandbox_error( }; } + // similarly, if the command timed out, we can simply return this failure to the model + if matches!(error, SandboxErr::Timeout) { + return ResponseInputItem::FunctionCallOutput { + call_id, + output: FunctionCallOutputPayload { + content: format!( + "command timed out after {} milliseconds", + params.timeout_duration().as_millis() + ), + success: Some(false), + }, + }; + } + // Note that when `error` is `SandboxErr::Denied`, it could be a false // positive. That is, it may have exited with a non-zero exit code, not // because the sandbox denied it, but because that is its expected behavior, diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index dce02cc5e2..e98aeeaece 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -51,6 +51,12 @@ pub struct ExecParams { pub env: HashMap, } +impl ExecParams { + pub fn timeout_duration(&self) -> Duration { + Duration::from_millis(self.timeout_ms.unwrap_or(DEFAULT_TIMEOUT_MS)) + } +} + #[derive(Clone, Copy, Debug, PartialEq)] pub enum SandboxType { None, @@ -83,11 +89,9 @@ pub async fn process_exec_tool_call( { SandboxType::None => exec(params, sandbox_policy, ctrl_c, stdout_stream.clone()).await, SandboxType::MacosSeatbelt => { + let timeout = params.timeout_duration(); let ExecParams { - command, - cwd, - timeout_ms, - env, + command, cwd, env, .. } = params; let child = spawn_command_under_seatbelt( command, @@ -97,14 +101,12 @@ pub async fn process_exec_tool_call( env, ) .await?; - consume_truncated_output(child, ctrl_c, timeout_ms, stdout_stream.clone()).await + consume_truncated_output(child, ctrl_c, timeout, stdout_stream.clone()).await } SandboxType::LinuxSeccomp => { + let timeout = params.timeout_duration(); let ExecParams { - command, - cwd, - timeout_ms, - env, + command, cwd, env, .. } = params; let codex_linux_sandbox_exe = codex_linux_sandbox_exe @@ -120,7 +122,7 @@ pub async fn process_exec_tool_call( ) .await?; - consume_truncated_output(child, ctrl_c, timeout_ms, stdout_stream).await + consume_truncated_output(child, ctrl_c, timeout, stdout_stream).await } }; let duration = start.elapsed(); @@ -255,16 +257,16 @@ pub struct ExecToolCallOutput { } async fn exec( - ExecParams { - command, - cwd, - timeout_ms, - env, - }: ExecParams, + params: ExecParams, sandbox_policy: &SandboxPolicy, ctrl_c: Arc, stdout_stream: Option, ) -> Result { + let timeout = params.timeout_duration(); + let ExecParams { + command, cwd, env, .. + } = params; + let (program, args) = command.split_first().ok_or_else(|| { CodexErr::Io(io::Error::new( io::ErrorKind::InvalidInput, @@ -282,7 +284,7 @@ async fn exec( env, ) .await?; - consume_truncated_output(child, ctrl_c, timeout_ms, stdout_stream).await + consume_truncated_output(child, ctrl_c, timeout, stdout_stream).await } /// Consumes the output of a child process, truncating it so it is suitable for @@ -290,7 +292,7 @@ async fn exec( pub(crate) async fn consume_truncated_output( mut child: Child, ctrl_c: Arc, - timeout_ms: Option, + timeout: Duration, stdout_stream: Option, ) -> Result { // Both stdout and stderr were configured with `Stdio::piped()` @@ -324,7 +326,6 @@ pub(crate) async fn consume_truncated_output( )); let interrupted = ctrl_c.notified(); - let timeout = Duration::from_millis(timeout_ms.unwrap_or(DEFAULT_TIMEOUT_MS)); let exit_status = tokio::select! { result = tokio::time::timeout(timeout, child.wait()) => { match result { From afa8f0d6177ad7016b3311f1ce1531fec9466973 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 5 Aug 2025 19:19:36 -0700 Subject: [PATCH 0027/1309] fix: exit cleanly when ShutdownComplete is received (#1864) Previous to this PR, `ShutdownComplete` was not being handled correctly in `codex exec`, so it always ended up printing the following to stderr: ``` ERROR codex_exec: Error receiving event: InternalAgentDied ``` Because we were not breaking out of the loop for `ShutdownComplete`, inevitably `codex.next_event()` would get called again and `rx_event.recv()` would fail and the error would get mapped to `InternalAgentDied`: https://github.com/openai/codex/blob/ea7d3f27bdc1da61df979419515889f64f36c5ce/codex-rs/core/src/codex.rs#L190-L197 For reference, https://github.com/openai/codex/pull/1647 introduced the `ShutdownComplete` variant. --- codex-rs/exec/src/lib.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 288b6177e5..06df2aebca 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -216,10 +216,16 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any res = codex.next_event() => match res { Ok(event) => { debug!("Received event: {event:?}"); + + let is_shutdown_complete = matches!(event.msg, EventMsg::ShutdownComplete); if let Err(e) = tx.send(event) { error!("Error sending event: {e:?}"); break; } + if is_shutdown_complete { + info!("Received shutdown event, exiting event loop."); + break; + } }, Err(e) => { error!("Error receiving event: {e:?}"); From aff97ed7dda1cdfca3debf048b9b05902fbd4c15 Mon Sep 17 00:00:00 2001 From: Dylan Date: Tue, 5 Aug 2025 19:27:52 -0700 Subject: [PATCH 0028/1309] [core] Separate tools config from openai client (#1858) ## Summary In an effort to make tools easier to work with and more configurable, I'm introducing `ToolConfig` and updating `Prompt` to take in a general list of Tools. I think this is simpler and better for a few reasons: - We can easily assemble tools from various sources (our own harness, mcp servers, etc.) and we can consolidate the logic for constructing the logic in one place that is separate from serialization. - client.rs no longer needs arbitrary config values, it just takes in a list of tools to serialize A hefty portion of the PR is now updating our conversion of `mcp_types::Tool` to `OpenAITool`, but considering that @bolinfest accurately called this out as a TODO long ago, I think it's time we tackled it. ## Testing - [x] Experimented locally, no changes, as expected - [x] Added additional unit tests - [x] Responded to rust-review --- codex-rs/core/src/chat_completions.rs | 4 +- codex-rs/core/src/client.rs | 7 +- codex-rs/core/src/client_common.rs | 9 +- codex-rs/core/src/codex.rs | 14 +- codex-rs/core/src/lib.rs | 1 - codex-rs/core/src/openai_tools.rs | 277 +++++++++++++++++++++----- codex-rs/core/src/plan_tool.rs | 12 +- 7 files changed, 250 insertions(+), 74 deletions(-) diff --git a/codex-rs/core/src/chat_completions.rs b/codex-rs/core/src/chat_completions.rs index 956dcebda9..98ef7f26cc 100644 --- a/codex-rs/core/src/chat_completions.rs +++ b/codex-rs/core/src/chat_completions.rs @@ -32,7 +32,6 @@ use crate::util::backoff; pub(crate) async fn stream_chat_completions( prompt: &Prompt, model_family: &ModelFamily, - include_plan_tool: bool, client: &reqwest::Client, provider: &ModelProviderInfo, ) -> Result { @@ -112,8 +111,7 @@ pub(crate) async fn stream_chat_completions( } } - let tools_json = - create_tools_json_for_chat_completions_api(prompt, model_family, include_plan_tool)?; + let tools_json = create_tools_json_for_chat_completions_api(&prompt.tools)?; let payload = json!({ "model": model_family.slug, "messages": messages, diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 514e683e53..e4bb30da26 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -83,7 +83,6 @@ impl ModelClient { let response_stream = stream_chat_completions( prompt, &self.config.model_family, - self.config.include_plan_tool, &self.client, &self.provider, ) @@ -132,11 +131,7 @@ impl ModelClient { let store = prompt.store && auth_mode != Some(AuthMode::ChatGPT); let full_instructions = prompt.get_full_instructions(&self.config.model_family); - let tools_json = create_tools_json_for_responses_api( - prompt, - &self.config.model_family, - self.config.include_plan_tool, - )?; + let tools_json = create_tools_json_for_responses_api(&prompt.tools)?; let reasoning = create_reasoning_param_for_request( &self.config.model_family, self.effort, diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index 8b845a52e6..60164f5fde 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -3,12 +3,12 @@ use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; use crate::error::Result; use crate::model_family::ModelFamily; use crate::models::ResponseItem; +use crate::openai_tools::OpenAiTool; use crate::protocol::TokenUsage; use codex_apply_patch::APPLY_PATCH_TOOL_INSTRUCTIONS; use futures::Stream; use serde::Serialize; use std::borrow::Cow; -use std::collections::HashMap; use std::pin::Pin; use std::task::Context; use std::task::Poll; @@ -33,10 +33,9 @@ pub struct Prompt { /// Whether to store response on server side (disable_response_storage = !store). pub store: bool, - /// Additional tools sourced from external MCP servers. Note each key is - /// the "fully qualified" tool name (i.e., prefixed with the server name), - /// which should be reported to the model in place of Tool::name. - pub extra_tools: HashMap, + /// Tools available to the model, including additional tools sourced from + /// external MCP servers. + pub tools: Vec, /// Optional override for the built-in BASE_INSTRUCTIONS. pub base_instructions_override: Option, diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 81ec3cab6e..9c3a25c200 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -61,6 +61,8 @@ use crate::models::ReasoningItemReasoningSummary; use crate::models::ResponseInputItem; use crate::models::ResponseItem; use crate::models::ShellToolCallParams; +use crate::openai_tools::ToolsConfig; +use crate::openai_tools::get_openai_tools; use crate::plan_tool::handle_update_plan; use crate::project_doc::get_user_instructions; use crate::protocol::AgentMessageDeltaEvent; @@ -216,6 +218,7 @@ pub(crate) struct Session { shell_environment_policy: ShellEnvironmentPolicy, pub(crate) writable_roots: Mutex>, disable_response_storage: bool, + tools_config: ToolsConfig, /// Manager for external MCP servers/tools. mcp_connection_manager: McpConnectionManager, @@ -810,6 +813,7 @@ async fn submission_loop( let default_shell = shell::default_user_shell().await; sess = Some(Arc::new(Session { client, + tools_config: ToolsConfig::new(&config.model_family, config.include_plan_tool), tx_event: tx_event.clone(), ctrl_c: Arc::clone(&ctrl_c), user_instructions, @@ -1204,12 +1208,16 @@ async fn run_turn( sub_id: String, input: Vec, ) -> CodexResult> { - let extra_tools = sess.mcp_connection_manager.list_all_tools(); + let tools = get_openai_tools( + &sess.tools_config, + Some(sess.mcp_connection_manager.list_all_tools()), + ); + let prompt = Prompt { input, user_instructions: sess.user_instructions.clone(), store: !sess.disable_response_storage, - extra_tools, + tools, base_instructions_override: sess.base_instructions.clone(), }; @@ -1436,7 +1444,7 @@ async fn run_compact_task( input: turn_input, user_instructions: None, store: !sess.disable_response_storage, - extra_tools: HashMap::new(), + tools: Vec::new(), base_instructions_override: Some(compact_instructions.clone()), }; diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index d072613e10..c728bd3125 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -48,6 +48,5 @@ pub mod spawn; pub mod turn_diff_tracker; mod user_notification; pub mod util; - pub use apply_patch::CODEX_APPLY_PATCH_ARG1; pub use safety::get_platform_sandbox; diff --git a/codex-rs/core/src/openai_tools.rs b/codex-rs/core/src/openai_tools.rs index 7d4bf4aa1a..1dac70819e 100644 --- a/codex-rs/core/src/openai_tools.rs +++ b/codex-rs/core/src/openai_tools.rs @@ -1,22 +1,26 @@ +use serde::Deserialize; use serde::Serialize; use serde_json::json; use std::collections::BTreeMap; +use std::collections::HashMap; -use crate::client_common::Prompt; use crate::model_family::ModelFamily; use crate::plan_tool::PLAN_TOOL; -#[derive(Debug, Clone, Serialize)] -pub(crate) struct ResponsesApiTool { - pub(crate) name: &'static str, - pub(crate) description: &'static str, +#[derive(Debug, Clone, Serialize, PartialEq)] +pub struct ResponsesApiTool { + pub(crate) name: String, + pub(crate) description: String, + /// TODO: Validation. When strict is set to true, the JSON schema, + /// `required` and `additional_properties` must be present. All fields in + /// `properties` must be present in `required`. pub(crate) strict: bool, pub(crate) parameters: JsonSchema, } /// When serialized as JSON, this produces a valid "Tool" in the OpenAI /// Responses API. -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, PartialEq)] #[serde(tag = "type")] pub(crate) enum OpenAiTool { #[serde(rename = "function")] @@ -25,8 +29,35 @@ pub(crate) enum OpenAiTool { LocalShell {}, } +#[derive(Debug, Clone)] +pub enum ConfigShellToolType { + DefaultShell, + LocalShell, +} + +#[derive(Debug, Clone)] +pub struct ToolsConfig { + pub shell_type: ConfigShellToolType, + pub plan_tool: bool, +} + +impl ToolsConfig { + pub fn new(model_family: &ModelFamily, include_plan_tool: bool) -> Self { + let shell_type = if model_family.uses_local_shell_tool { + ConfigShellToolType::LocalShell + } else { + ConfigShellToolType::DefaultShell + }; + + Self { + shell_type, + plan_tool: include_plan_tool, + } + } +} + /// Generic JSON‑Schema subset needed for our tool definitions -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(tag = "type", rename_all = "lowercase")] pub(crate) enum JsonSchema { String, @@ -36,13 +67,17 @@ pub(crate) enum JsonSchema { }, Object { properties: BTreeMap, - required: &'static [&'static str], - #[serde(rename = "additionalProperties")] - additional_properties: bool, + #[serde(skip_serializing_if = "Option::is_none")] + required: Option>, + #[serde( + rename = "additionalProperties", + skip_serializing_if = "Option::is_none" + )] + additional_properties: Option, }, } -fn create_shell_tool() -> OpenAiTool { +pub(crate) fn create_shell_tool() -> OpenAiTool { let mut properties = BTreeMap::new(); properties.insert( "command".to_string(), @@ -54,13 +89,13 @@ fn create_shell_tool() -> OpenAiTool { properties.insert("timeout".to_string(), JsonSchema::Number); OpenAiTool::Function(ResponsesApiTool { - name: "shell", - description: "Runs a shell command and returns its output", + name: "shell".to_string(), + description: "Runs a shell command and returns its output".to_string(), strict: false, parameters: JsonSchema::Object { properties, - required: &["command"], - additional_properties: false, + required: Some(vec!["command".to_string()]), + additional_properties: Some(false), }, }) } @@ -69,31 +104,13 @@ fn create_shell_tool() -> OpenAiTool { /// Responses API: /// https://platform.openai.com/docs/guides/function-calling?api-mode=responses pub(crate) fn create_tools_json_for_responses_api( - prompt: &Prompt, - model_family: &ModelFamily, - include_plan_tool: bool, + tools: &Vec, ) -> crate::error::Result> { - // Assemble tool list: built-in tools + any extra tools from the prompt. - let mut openai_tools = vec![create_shell_tool()]; - if model_family.uses_local_shell_tool { - openai_tools.push(OpenAiTool::LocalShell {}); - } + let mut tools_json = Vec::new(); - let mut tools_json = Vec::with_capacity(openai_tools.len() + prompt.extra_tools.len() + 1); - for tool in openai_tools.iter() { + for tool in tools { tools_json.push(serde_json::to_value(tool)?); } - tools_json.extend( - prompt - .extra_tools - .clone() - .into_iter() - .map(|(name, tool)| mcp_tool_to_openai_tool(name, tool)), - ); - - if include_plan_tool { - tools_json.push(serde_json::to_value(PLAN_TOOL.clone())?); - } Ok(tools_json) } @@ -102,14 +119,11 @@ pub(crate) fn create_tools_json_for_responses_api( /// Chat Completions API: /// https://platform.openai.com/docs/guides/function-calling?api-mode=chat pub(crate) fn create_tools_json_for_chat_completions_api( - prompt: &Prompt, - model_family: &ModelFamily, - include_plan_tool: bool, + tools: &Vec, ) -> crate::error::Result> { // We start with the JSON for the Responses API and than rewrite it to match // the chat completions tool call format. - let responses_api_tools_json = - create_tools_json_for_responses_api(prompt, model_family, include_plan_tool)?; + let responses_api_tools_json = create_tools_json_for_responses_api(tools)?; let tools_json = responses_api_tools_json .into_iter() .filter_map(|mut tool| { @@ -132,10 +146,10 @@ pub(crate) fn create_tools_json_for_chat_completions_api( Ok(tools_json) } -fn mcp_tool_to_openai_tool( +pub(crate) fn mcp_tool_to_openai_tool( fully_qualified_name: String, tool: mcp_types::Tool, -) -> serde_json::Value { +) -> Result { let mcp_types::Tool { description, mut input_schema, @@ -150,12 +164,175 @@ fn mcp_tool_to_openai_tool( input_schema.properties = Some(serde_json::Value::Object(serde_json::Map::new())); } - // TODO(mbolin): Change the contract of this function to return - // ResponsesApiTool. - json!({ - "name": fully_qualified_name, - "description": description, - "parameters": input_schema, - "type": "function", + let serialized_input_schema = serde_json::to_value(input_schema)?; + let input_schema = serde_json::from_value::(serialized_input_schema)?; + + Ok(ResponsesApiTool { + name: fully_qualified_name, + description: description.unwrap_or_default(), + strict: false, + parameters: input_schema, }) } + +/// Returns a list of OpenAiTools based on the provided config and MCP tools. +/// Note that the keys of mcp_tools should be fully qualified names. See +/// [`McpConnectionManager`] for more details. +pub(crate) fn get_openai_tools( + config: &ToolsConfig, + mcp_tools: Option>, +) -> Vec { + let mut tools: Vec = Vec::new(); + + match config.shell_type { + ConfigShellToolType::DefaultShell => { + tools.push(create_shell_tool()); + } + ConfigShellToolType::LocalShell => { + tools.push(OpenAiTool::LocalShell {}); + } + } + + if config.plan_tool { + tools.push(PLAN_TOOL.clone()); + } + + if let Some(mcp_tools) = mcp_tools { + for (name, tool) in mcp_tools { + match mcp_tool_to_openai_tool(name.clone(), tool.clone()) { + Ok(converted_tool) => tools.push(OpenAiTool::Function(converted_tool)), + Err(e) => { + tracing::error!("Failed to convert {name:?} MCP tool to OpenAI tool: {e:?}"); + } + } + } + } + + tools +} + +#[cfg(test)] +#[allow(clippy::expect_used)] +mod tests { + use crate::model_family::find_family_for_model; + use mcp_types::ToolInputSchema; + + use super::*; + + fn assert_eq_tool_names(tools: &[OpenAiTool], expected_names: &[&str]) { + let tool_names = tools + .iter() + .map(|tool| match tool { + OpenAiTool::Function(ResponsesApiTool { name, .. }) => name, + OpenAiTool::LocalShell {} => "local_shell", + }) + .collect::>(); + + assert_eq!( + tool_names.len(), + expected_names.len(), + "tool_name mismatch, {tool_names:?}, {expected_names:?}", + ); + for (name, expected_name) in tool_names.iter().zip(expected_names.iter()) { + assert_eq!( + name, expected_name, + "tool_name mismatch, {name:?}, {expected_name:?}" + ); + } + } + + #[test] + fn test_get_openai_tools() { + let model_family = find_family_for_model("codex-mini-latest") + .expect("codex-mini-latest should be a valid model family"); + let config = ToolsConfig::new(&model_family, true); + let tools = get_openai_tools(&config, Some(HashMap::new())); + + assert_eq_tool_names(&tools, &["local_shell", "update_plan"]); + } + + #[test] + fn test_get_openai_tools_default_shell() { + let model_family = find_family_for_model("o3").expect("o3 should be a valid model family"); + let config = ToolsConfig::new(&model_family, true); + let tools = get_openai_tools(&config, Some(HashMap::new())); + + assert_eq_tool_names(&tools, &["shell", "update_plan"]); + } + + #[test] + fn test_get_openai_tools_mcp_tools() { + let model_family = find_family_for_model("o3").expect("o3 should be a valid model family"); + let config = ToolsConfig::new(&model_family, false); + let tools = get_openai_tools( + &config, + Some(HashMap::from([( + "test_server/do_something_cool".to_string(), + mcp_types::Tool { + name: "do_something_cool".to_string(), + input_schema: ToolInputSchema { + properties: Some(serde_json::json!({ + "string_argument": { + "type": "string", + }, + "number_argument": { + "type": "number", + }, + "object_argument": { + "type": "object", + "properties": { + "string_property": { "type": "string" }, + "number_property": { "type": "number" }, + }, + "required": [ + "string_property", + "number_property" + ], + "additionalProperties": Some(false), + }, + })), + required: None, + r#type: "object".to_string(), + }, + output_schema: None, + title: None, + annotations: None, + description: Some("Do something cool".to_string()), + }, + )])), + ); + + assert_eq_tool_names(&tools, &["shell", "test_server/do_something_cool"]); + + assert_eq!( + tools[1], + OpenAiTool::Function(ResponsesApiTool { + name: "test_server/do_something_cool".to_string(), + parameters: JsonSchema::Object { + properties: BTreeMap::from([ + ("string_argument".to_string(), JsonSchema::String), + ("number_argument".to_string(), JsonSchema::Number), + ( + "object_argument".to_string(), + JsonSchema::Object { + properties: BTreeMap::from([ + ("string_property".to_string(), JsonSchema::String), + ("number_property".to_string(), JsonSchema::Number), + ]), + required: Some(vec![ + "string_property".to_string(), + "number_property".to_string(), + ]), + additional_properties: Some(false), + }, + ), + ]), + required: None, + additional_properties: None, + }, + description: "Do something cool".to_string(), + strict: false, + }) + ); + } +} diff --git a/codex-rs/core/src/plan_tool.rs b/codex-rs/core/src/plan_tool.rs index dbddb8b5eb..cfc26a4021 100644 --- a/codex-rs/core/src/plan_tool.rs +++ b/codex-rs/core/src/plan_tool.rs @@ -45,8 +45,8 @@ pub(crate) static PLAN_TOOL: LazyLock = LazyLock::new(|| { let plan_items_schema = JsonSchema::Array { items: Box::new(JsonSchema::Object { properties: plan_item_props, - required: &["step", "status"], - additional_properties: false, + required: Some(vec!["step".to_string(), "status".to_string()]), + additional_properties: Some(false), }), }; @@ -55,7 +55,7 @@ pub(crate) static PLAN_TOOL: LazyLock = LazyLock::new(|| { properties.insert("plan".to_string(), plan_items_schema); OpenAiTool::Function(ResponsesApiTool { - name: "update_plan", + name: "update_plan".to_string(), description: r#"Use the update_plan tool to keep the user updated on the current plan for the task. After understanding the user's task, call the update_plan tool with an initial plan. An example of a plan: 1. Explore the codebase to find relevant files (status: in_progress) @@ -66,12 +66,12 @@ Until all the steps are finished, there should always be exactly one in_progress Call the update_plan tool whenever you finish a step, marking the completed step as `completed` and marking the next step as `in_progress`. Before running a command, consider whether or not you have completed the previous step, and make sure to mark it as completed before moving on to the next step. Sometimes, you may need to change plans in the middle of a task: call `update_plan` with the updated plan and make sure to provide an `explanation` of the rationale when doing so. -When all steps are completed, call update_plan one last time with all steps marked as `completed`."#, +When all steps are completed, call update_plan one last time with all steps marked as `completed`."#.to_string(), strict: false, parameters: JsonSchema::Object { properties, - required: &["plan"], - additional_properties: false, + required: Some(vec!["plan".to_string()]), + additional_properties: Some(false), }, }) }); From 725dd6be6a581eb304b6cae786bc848cc780e796 Mon Sep 17 00:00:00 2001 From: Dylan Date: Tue, 5 Aug 2025 20:44:20 -0700 Subject: [PATCH 0029/1309] [approval_policy] Add OnRequest approval_policy (#1865) ## Summary A split-up PR of #1763 , stacked on top of a tools refactor #1858 to make the change clearer. From the previous summary: > Let's try something new: tell the model about the sandbox, and let it decide when it will need to break the sandbox. Some local testing suggests that it works pretty well with zero iteration on the prompt! ## Testing - [x] Added unit tests - [x] Tested locally and it appears to work smoothly! --- codex-rs/common/src/approval_mode_cli_arg.rs | 4 + codex-rs/config.md | 8 + codex-rs/core/src/codex.rs | 48 +++-- codex-rs/core/src/exec.rs | 2 + codex-rs/core/src/models.rs | 6 + codex-rs/core/src/openai_tools.rs | 194 +++++++++++++++++-- codex-rs/core/src/plan_tool.rs | 13 +- codex-rs/core/src/protocol.rs | 3 + codex-rs/core/src/safety.rs | 67 ++++++- codex-rs/core/src/shell.rs | 2 + codex-rs/core/tests/exec.rs | 2 + codex-rs/core/tests/exec_stream_events.rs | 4 + codex-rs/linux-sandbox/tests/landlock.rs | 4 + 13 files changed, 320 insertions(+), 37 deletions(-) diff --git a/codex-rs/common/src/approval_mode_cli_arg.rs b/codex-rs/common/src/approval_mode_cli_arg.rs index a74ceb2b81..e8c0682687 100644 --- a/codex-rs/common/src/approval_mode_cli_arg.rs +++ b/codex-rs/common/src/approval_mode_cli_arg.rs @@ -18,6 +18,9 @@ pub enum ApprovalModeCliArg { /// will escalate to the user to ask for un-sandboxed execution. OnFailure, + /// The model decides when to ask the user for approval. + OnRequest, + /// Never ask for user approval /// Execution failures are immediately returned to the model. Never, @@ -28,6 +31,7 @@ impl From for AskForApproval { match value { ApprovalModeCliArg::Untrusted => AskForApproval::UnlessTrusted, ApprovalModeCliArg::OnFailure => AskForApproval::OnFailure, + ApprovalModeCliArg::OnRequest => AskForApproval::OnRequest, ApprovalModeCliArg::Never => AskForApproval::Never, } } diff --git a/codex-rs/config.md b/codex-rs/config.md index 992fe1aacc..f93a35ebca 100644 --- a/codex-rs/config.md +++ b/codex-rs/config.md @@ -148,12 +148,20 @@ Determines when the user should be prompted to approve whether Codex can execute approval_policy = "untrusted" ``` +If you want to be notified whenever a command fails, use "on-failure": ```toml # If the command fails when run in the sandbox, Codex asks for permission to # retry the command outside the sandbox. approval_policy = "on-failure" ``` +If you want the model to run until it decides that it needs to ask you for escalated permissions, use "on-request": +```toml +# The model decides when to escalate +approval_policy = "on-request" +``` + +Alternatively, you can have the model run until it is done, and never ask to run a command with escalated permissions: ```toml # User is never prompted: if the command fails, Codex will automatically try # something out. Note the `exec` subcommand always uses this mode. diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 9c3a25c200..a7ab664ee0 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -813,7 +813,12 @@ async fn submission_loop( let default_shell = shell::default_user_shell().await; sess = Some(Arc::new(Session { client, - tools_config: ToolsConfig::new(&config.model_family, config.include_plan_tool), + tools_config: ToolsConfig::new( + &config.model_family, + approval_policy, + sandbox_policy.clone(), + config.include_plan_tool, + ), tx_event: tx_event.clone(), ctrl_c: Arc::clone(&ctrl_c), user_instructions, @@ -1588,6 +1593,8 @@ async fn handle_response_item( command: action.command, workdir: action.working_directory, timeout_ms: action.timeout_ms, + with_escalated_permissions: None, + justification: None, }; let effective_call_id = match (call_id, id) { (Some(call_id), _) => call_id, @@ -1676,6 +1683,8 @@ fn to_exec_params(params: ShellToolCallParams, sess: &Session) -> ExecParams { cwd: sess.resolve_path(params.workdir.clone()), timeout_ms: params.timeout_ms, env: create_env(&sess.shell_environment_policy), + with_escalated_permissions: params.with_escalated_permissions, + justification: params.justification, } } @@ -1776,13 +1785,19 @@ async fn handle_container_exec_with_params( cwd: cwd.clone(), timeout_ms: params.timeout_ms, env: HashMap::new(), + with_escalated_permissions: params.with_escalated_permissions, + justification: params.justification.clone(), }; let safety = if *user_explicitly_approved_this_action { SafetyCheck::AutoApprove { sandbox_type: SandboxType::None, } } else { - assess_safety_for_untrusted_command(sess.approval_policy, &sess.sandbox_policy) + assess_safety_for_untrusted_command( + sess.approval_policy, + &sess.sandbox_policy, + params.with_escalated_permissions.unwrap_or(false), + ) }; ( params, @@ -1798,6 +1813,7 @@ async fn handle_container_exec_with_params( sess.approval_policy, &sess.sandbox_policy, &state.approved_commands, + params.with_escalated_permissions.unwrap_or(false), ) }; let command_for_display = params.command.clone(); @@ -1814,7 +1830,7 @@ async fn handle_container_exec_with_params( call_id.clone(), params.command.clone(), params.cwd.clone(), - None, + params.justification.clone(), ) .await; match rx_approve.await.unwrap_or_default() { @@ -1952,17 +1968,21 @@ async fn handle_sandbox_error( let cwd = exec_command_context.cwd.clone(); let is_apply_patch = exec_command_context.apply_patch.is_some(); - // Early out if the user never wants to be asked for approval; just return to the model immediately - if sess.approval_policy == AskForApproval::Never { - return ResponseInputItem::FunctionCallOutput { - call_id, - output: FunctionCallOutputPayload { - content: format!( - "failed in sandbox {sandbox_type:?} with execution error: {error}" - ), - success: Some(false), - }, - }; + // Early out if either the user never wants to be asked for approval, or + // we're letting the model manage escalation requests. Otherwise, continue + match sess.approval_policy { + AskForApproval::Never | AskForApproval::OnRequest => { + return ResponseInputItem::FunctionCallOutput { + call_id, + output: FunctionCallOutputPayload { + content: format!( + "failed in sandbox {sandbox_type:?} with execution error: {error}" + ), + success: Some(false), + }, + }; + } + AskForApproval::UnlessTrusted | AskForApproval::OnFailure => (), } // similarly, if the command timed out, we can simply return this failure to the model diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index e98aeeaece..10606b6821 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -49,6 +49,8 @@ pub struct ExecParams { pub cwd: PathBuf, pub timeout_ms: Option, pub env: HashMap, + pub with_escalated_permissions: Option, + pub justification: Option, } impl ExecParams { diff --git a/codex-rs/core/src/models.rs b/codex-rs/core/src/models.rs index fb48b53070..e052bc43a4 100644 --- a/codex-rs/core/src/models.rs +++ b/codex-rs/core/src/models.rs @@ -191,6 +191,10 @@ pub struct ShellToolCallParams { // The wire format uses `timeout`, which has ambiguous units, so we use // `timeout_ms` as the field name so it is clear in code. pub timeout_ms: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub with_escalated_permissions: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub justification: Option, } #[derive(Debug, Clone, PartialEq)] @@ -302,6 +306,8 @@ mod tests { command: vec!["ls".to_string(), "-l".to_string()], workdir: Some("/tmp".to_string()), timeout_ms: Some(1000), + with_escalated_permissions: None, + justification: None, }, params ); diff --git a/codex-rs/core/src/openai_tools.rs b/codex-rs/core/src/openai_tools.rs index 1dac70819e..1c92c07c12 100644 --- a/codex-rs/core/src/openai_tools.rs +++ b/codex-rs/core/src/openai_tools.rs @@ -6,6 +6,8 @@ use std::collections::HashMap; use crate::model_family::ModelFamily; use crate::plan_tool::PLAN_TOOL; +use crate::protocol::AskForApproval; +use crate::protocol::SandboxPolicy; #[derive(Debug, Clone, Serialize, PartialEq)] pub struct ResponsesApiTool { @@ -32,6 +34,7 @@ pub(crate) enum OpenAiTool { #[derive(Debug, Clone)] pub enum ConfigShellToolType { DefaultShell, + ShellWithRequest { sandbox_policy: SandboxPolicy }, LocalShell, } @@ -42,12 +45,22 @@ pub struct ToolsConfig { } impl ToolsConfig { - pub fn new(model_family: &ModelFamily, include_plan_tool: bool) -> Self { - let shell_type = if model_family.uses_local_shell_tool { + pub fn new( + model_family: &ModelFamily, + approval_policy: AskForApproval, + sandbox_policy: SandboxPolicy, + include_plan_tool: bool, + ) -> Self { + let mut shell_type = if model_family.uses_local_shell_tool { ConfigShellToolType::LocalShell } else { ConfigShellToolType::DefaultShell }; + if matches!(approval_policy, AskForApproval::OnRequest) { + shell_type = ConfigShellToolType::ShellWithRequest { + sandbox_policy: sandbox_policy.clone(), + } + } Self { shell_type, @@ -60,10 +73,23 @@ impl ToolsConfig { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(tag = "type", rename_all = "lowercase")] pub(crate) enum JsonSchema { - String, - Number, + Boolean { + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + }, + String { + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + }, + Number { + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + }, Array { items: Box, + + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, }, Object { properties: BTreeMap, @@ -77,16 +103,23 @@ pub(crate) enum JsonSchema { }, } -pub(crate) fn create_shell_tool() -> OpenAiTool { +fn create_shell_tool() -> OpenAiTool { let mut properties = BTreeMap::new(); properties.insert( "command".to_string(), JsonSchema::Array { - items: Box::new(JsonSchema::String), + items: Box::new(JsonSchema::String { description: None }), + description: None, }, ); - properties.insert("workdir".to_string(), JsonSchema::String); - properties.insert("timeout".to_string(), JsonSchema::Number); + properties.insert( + "workdir".to_string(), + JsonSchema::String { description: None }, + ); + properties.insert( + "timeout".to_string(), + JsonSchema::Number { description: None }, + ); OpenAiTool::Function(ResponsesApiTool { name: "shell".to_string(), @@ -100,6 +133,105 @@ pub(crate) fn create_shell_tool() -> OpenAiTool { }) } +fn create_shell_tool_for_sandbox(sandbox_policy: &SandboxPolicy) -> OpenAiTool { + let mut properties = BTreeMap::new(); + properties.insert( + "command".to_string(), + JsonSchema::Array { + items: Box::new(JsonSchema::String { description: None }), + description: Some("The command to execute".to_string()), + }, + ); + properties.insert( + "workdir".to_string(), + JsonSchema::String { + description: Some("The working directory to execute the command in".to_string()), + }, + ); + properties.insert( + "timeout".to_string(), + JsonSchema::Number { + description: Some("The timeout for the command in milliseconds".to_string()), + }, + ); + + if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) { + properties.insert( + "with_escalated_permissions".to_string(), + JsonSchema::Boolean { + description: Some("Whether to request escalated permissions. Set to true if command needs to be run without sandbox restrictions".to_string()), + }, + ); + properties.insert( + "justification".to_string(), + JsonSchema::String { + description: Some("Only set if ask_for_escalated_permissions is true. 1-sentence explanation of why we want to run this command.".to_string()), + }, + ); + } + + let description = match sandbox_policy { + SandboxPolicy::WorkspaceWrite { + network_access, + .. + } => { + format!( + r#" +The shell tool is used to execute shell commands. +- When invoking the shell tool, your call will be running in a landlock sandbox, and some shell commands will require escalated privileges: + - Types of actions that require escalated privileges: + - Reading files outside the current directory + - Writing files outside the current directory, and protected folders like .git or .env{} + - Examples of commands that require escalated privileges: + - git commit + - npm install or pnpm install + - cargo build + - cargo test +- When invoking a command that will require escalated privileges: + - Provide the with_escalated_permissions parameter with the boolean value true + - Include a short, 1 sentence explanation for why we need to run with_escalated_permissions in the justification parameter."#, + if !network_access { + "\n - Commands that require network access\n" + } else { + "" + } + ) + } + SandboxPolicy::DangerFullAccess => { + "Runs a shell command and returns its output.".to_string() + } + SandboxPolicy::ReadOnly => { + r#" +The shell tool is used to execute shell commands. +- When invoking the shell tool, your call will be running in a landlock sandbox, and some shell commands (including apply_patch) will require escalated permissions: + - Types of actions that require escalated privileges: + - Reading files outside the current directory + - Writing files + - Applying patches + - Examples of commands that require escalated privileges: + - apply_patch + - git commit + - npm install or pnpm install + - cargo build + - cargo test +- When invoking a command that will require escalated privileges: + - Provide the with_escalated_permissions parameter with the boolean value true + - Include a short, 1 sentence explanation for why we need to run with_escalated_permissions in the justification parameter"#.to_string() + } + }; + + OpenAiTool::Function(ResponsesApiTool { + name: "shell".to_string(), + description, + strict: false, + parameters: JsonSchema::Object { + properties, + required: Some(vec!["command".to_string()]), + additional_properties: Some(false), + }, + }) +} + /// Returns JSON values that are compatible with Function Calling in the /// Responses API: /// https://platform.openai.com/docs/guides/function-calling?api-mode=responses @@ -184,10 +316,13 @@ pub(crate) fn get_openai_tools( ) -> Vec { let mut tools: Vec = Vec::new(); - match config.shell_type { + match &config.shell_type { ConfigShellToolType::DefaultShell => { tools.push(create_shell_tool()); } + ConfigShellToolType::ShellWithRequest { sandbox_policy } => { + tools.push(create_shell_tool_for_sandbox(sandbox_policy)); + } ConfigShellToolType::LocalShell => { tools.push(OpenAiTool::LocalShell {}); } @@ -245,7 +380,12 @@ mod tests { fn test_get_openai_tools() { let model_family = find_family_for_model("codex-mini-latest") .expect("codex-mini-latest should be a valid model family"); - let config = ToolsConfig::new(&model_family, true); + let config = ToolsConfig::new( + &model_family, + AskForApproval::Never, + SandboxPolicy::ReadOnly, + true, + ); let tools = get_openai_tools(&config, Some(HashMap::new())); assert_eq_tool_names(&tools, &["local_shell", "update_plan"]); @@ -254,7 +394,12 @@ mod tests { #[test] fn test_get_openai_tools_default_shell() { let model_family = find_family_for_model("o3").expect("o3 should be a valid model family"); - let config = ToolsConfig::new(&model_family, true); + let config = ToolsConfig::new( + &model_family, + AskForApproval::Never, + SandboxPolicy::ReadOnly, + true, + ); let tools = get_openai_tools(&config, Some(HashMap::new())); assert_eq_tool_names(&tools, &["shell", "update_plan"]); @@ -263,7 +408,12 @@ mod tests { #[test] fn test_get_openai_tools_mcp_tools() { let model_family = find_family_for_model("o3").expect("o3 should be a valid model family"); - let config = ToolsConfig::new(&model_family, false); + let config = ToolsConfig::new( + &model_family, + AskForApproval::Never, + SandboxPolicy::ReadOnly, + false, + ); let tools = get_openai_tools( &config, Some(HashMap::from([( @@ -310,14 +460,26 @@ mod tests { name: "test_server/do_something_cool".to_string(), parameters: JsonSchema::Object { properties: BTreeMap::from([ - ("string_argument".to_string(), JsonSchema::String), - ("number_argument".to_string(), JsonSchema::Number), + ( + "string_argument".to_string(), + JsonSchema::String { description: None } + ), + ( + "number_argument".to_string(), + JsonSchema::Number { description: None } + ), ( "object_argument".to_string(), JsonSchema::Object { properties: BTreeMap::from([ - ("string_property".to_string(), JsonSchema::String), - ("number_property".to_string(), JsonSchema::Number), + ( + "string_property".to_string(), + JsonSchema::String { description: None } + ), + ( + "number_property".to_string(), + JsonSchema::Number { description: None } + ), ]), required: Some(vec![ "string_property".to_string(), diff --git a/codex-rs/core/src/plan_tool.rs b/codex-rs/core/src/plan_tool.rs index cfc26a4021..bba5363266 100644 --- a/codex-rs/core/src/plan_tool.rs +++ b/codex-rs/core/src/plan_tool.rs @@ -39,10 +39,14 @@ pub struct UpdatePlanArgs { pub(crate) static PLAN_TOOL: LazyLock = LazyLock::new(|| { let mut plan_item_props = BTreeMap::new(); - plan_item_props.insert("step".to_string(), JsonSchema::String); - plan_item_props.insert("status".to_string(), JsonSchema::String); + plan_item_props.insert("step".to_string(), JsonSchema::String { description: None }); + plan_item_props.insert( + "status".to_string(), + JsonSchema::String { description: None }, + ); let plan_items_schema = JsonSchema::Array { + description: Some("The list of steps".to_string()), items: Box::new(JsonSchema::Object { properties: plan_item_props, required: Some(vec!["step".to_string(), "status".to_string()]), @@ -51,7 +55,10 @@ pub(crate) static PLAN_TOOL: LazyLock = LazyLock::new(|| { }; let mut properties = BTreeMap::new(); - properties.insert("explanation".to_string(), JsonSchema::String); + properties.insert( + "explanation".to_string(), + JsonSchema::String { description: None }, + ); properties.insert("plan".to_string(), plan_items_schema); OpenAiTool::Function(ResponsesApiTool { diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs index aa330f6bae..9bf85ec49a 100644 --- a/codex-rs/core/src/protocol.rs +++ b/codex-rs/core/src/protocol.rs @@ -150,6 +150,9 @@ pub enum AskForApproval { /// the user to approve execution without a sandbox. OnFailure, + /// The model decides when to ask the user for approval. + OnRequest, + /// Never ask the user to approve commands. Failures are immediately returned /// to the model, and never escalated to the user for approval. Never, diff --git a/codex-rs/core/src/safety.rs b/codex-rs/core/src/safety.rs index 224705f8f3..860a728def 100644 --- a/codex-rs/core/src/safety.rs +++ b/codex-rs/core/src/safety.rs @@ -11,7 +11,7 @@ use crate::is_safe_command::is_known_safe_command; use crate::protocol::AskForApproval; use crate::protocol::SandboxPolicy; -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum SafetyCheck { AutoApprove { sandbox_type: SandboxType }, AskUser, @@ -31,7 +31,7 @@ pub fn assess_patch_safety( } match policy { - AskForApproval::OnFailure | AskForApproval::Never => { + AskForApproval::OnFailure | AskForApproval::Never | AskForApproval::OnRequest => { // Continue to see if this can be auto-approved. } // TODO(ragona): I'm not sure this is actually correct? I believe in this case @@ -76,6 +76,7 @@ pub fn assess_command_safety( approval_policy: AskForApproval, sandbox_policy: &SandboxPolicy, approved: &HashSet>, + with_escalated_permissions: bool, ) -> SafetyCheck { // A command is "trusted" because either: // - it belongs to a set of commands we consider "safe" by default, or @@ -96,12 +97,13 @@ pub fn assess_command_safety( }; } - assess_safety_for_untrusted_command(approval_policy, sandbox_policy) + assess_safety_for_untrusted_command(approval_policy, sandbox_policy, with_escalated_permissions) } pub(crate) fn assess_safety_for_untrusted_command( approval_policy: AskForApproval, sandbox_policy: &SandboxPolicy, + with_escalated_permissions: bool, ) -> SafetyCheck { use AskForApproval::*; use SandboxPolicy::*; @@ -113,9 +115,23 @@ pub(crate) fn assess_safety_for_untrusted_command( // commands. SafetyCheck::AskUser } - (OnFailure, DangerFullAccess) | (Never, DangerFullAccess) => SafetyCheck::AutoApprove { + (OnFailure, DangerFullAccess) + | (Never, DangerFullAccess) + | (OnRequest, DangerFullAccess) => SafetyCheck::AutoApprove { sandbox_type: SandboxType::None, }, + (OnRequest, ReadOnly) | (OnRequest, WorkspaceWrite { .. }) => { + if with_escalated_permissions { + SafetyCheck::AskUser + } else { + match get_platform_sandbox() { + Some(sandbox_type) => SafetyCheck::AutoApprove { sandbox_type }, + // Fall back to asking since the command is untrusted and + // we do not have a sandbox available + None => SafetyCheck::AskUser, + } + } + } (Never, ReadOnly) | (Never, WorkspaceWrite { .. }) | (OnFailure, ReadOnly) @@ -264,4 +280,47 @@ mod tests { &cwd, )) } + + #[test] + fn test_request_escalated_privileges() { + // Should not be a trusted command + let command = vec!["git commit".to_string()]; + let approval_policy = AskForApproval::OnRequest; + let sandbox_policy = SandboxPolicy::ReadOnly; + let approved: HashSet> = HashSet::new(); + let request_escalated_privileges = true; + + let safety_check = assess_command_safety( + &command, + approval_policy, + &sandbox_policy, + &approved, + request_escalated_privileges, + ); + + assert_eq!(safety_check, SafetyCheck::AskUser); + } + + #[test] + fn test_request_escalated_privileges_no_sandbox_fallback() { + let command = vec!["git".to_string(), "commit".to_string()]; + let approval_policy = AskForApproval::OnRequest; + let sandbox_policy = SandboxPolicy::ReadOnly; + let approved: HashSet> = HashSet::new(); + let request_escalated_privileges = false; + + let safety_check = assess_command_safety( + &command, + approval_policy, + &sandbox_policy, + &approved, + request_escalated_privileges, + ); + + let expected = match get_platform_sandbox() { + Some(sandbox_type) => SafetyCheck::AutoApprove { sandbox_type }, + None => SafetyCheck::AskUser, + }; + assert_eq!(safety_check, expected); + } } diff --git a/codex-rs/core/src/shell.rs b/codex-rs/core/src/shell.rs index 1e895a3701..de0764f75e 100644 --- a/codex-rs/core/src/shell.rs +++ b/codex-rs/core/src/shell.rs @@ -215,6 +215,8 @@ mod tests { "HOME".to_string(), temp_home.path().to_str().unwrap().to_string(), )]), + with_escalated_permissions: None, + justification: None, }, SandboxType::None, Arc::new(Notify::new()), diff --git a/codex-rs/core/tests/exec.rs b/codex-rs/core/tests/exec.rs index da169296ed..f1b9e78e67 100644 --- a/codex-rs/core/tests/exec.rs +++ b/codex-rs/core/tests/exec.rs @@ -28,6 +28,8 @@ async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>, should_be_ok: bool) { cwd: tmp.path().to_path_buf(), timeout_ms: Some(1000), env: HashMap::new(), + with_escalated_permissions: None, + justification: None, }; let ctrl_c = Arc::new(Notify::new()); diff --git a/codex-rs/core/tests/exec_stream_events.rs b/codex-rs/core/tests/exec_stream_events.rs index 50f6888f73..534b25513a 100644 --- a/codex-rs/core/tests/exec_stream_events.rs +++ b/codex-rs/core/tests/exec_stream_events.rs @@ -53,6 +53,8 @@ async fn test_exec_stdout_stream_events_echo() { cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), timeout_ms: Some(5_000), env: HashMap::new(), + with_escalated_permissions: None, + justification: None, }; let ctrl_c = Arc::new(Notify::new()); @@ -103,6 +105,8 @@ async fn test_exec_stderr_stream_events_echo() { cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), timeout_ms: Some(5_000), env: HashMap::new(), + with_escalated_permissions: None, + justification: None, }; let ctrl_c = Arc::new(Notify::new()); diff --git a/codex-rs/linux-sandbox/tests/landlock.rs b/codex-rs/linux-sandbox/tests/landlock.rs index 1375a4c686..041e64e208 100644 --- a/codex-rs/linux-sandbox/tests/landlock.rs +++ b/codex-rs/linux-sandbox/tests/landlock.rs @@ -44,6 +44,8 @@ async fn run_cmd(cmd: &[&str], writable_roots: &[PathBuf], timeout_ms: u64) { cwd: std::env::current_dir().expect("cwd should exist"), timeout_ms: Some(timeout_ms), env: create_env_from_core_vars(), + with_escalated_permissions: None, + justification: None, }; let sandbox_policy = SandboxPolicy::WorkspaceWrite { @@ -139,6 +141,8 @@ async fn assert_network_blocked(cmd: &[&str]) { // do not stall the suite. timeout_ms: Some(NETWORK_TIMEOUT_MS), env: create_env_from_core_vars(), + with_escalated_permissions: None, + justification: None, }; let sandbox_policy = SandboxPolicy::new_read_only_policy(); From 31dcae67dbb9f7941cf3a058ee050ad453cf42db Mon Sep 17 00:00:00 2001 From: aibrahim-oai Date: Tue, 5 Aug 2025 21:32:03 -0700 Subject: [PATCH 0030/1309] Remove Turndiff and Apply patch from the render (#1868) Make the tui more specific on what to render. Apply patch End and Turn diff needs special handling. Avoiding this issue: image --- codex-rs/tui/src/chatwidget.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 94bd2f121b..7751404f7d 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -492,11 +492,13 @@ impl ChatWidget<'_> { EventMsg::ShutdownComplete => { self.app_event_tx.send(AppEvent::ExitRequest); } - event => { - let text = format!("{event:?}"); - self.add_to_history(HistoryCell::new_background_event(text.clone())); - self.update_latest_log(text); + EventMsg::BackgroundEvent(event) => { + let message = event.message; + self.add_to_history(HistoryCell::new_background_event(message.clone())); + self.update_latest_log(message); } + // TODO: Think of how are we going to render these events. + EventMsg::PatchApplyEnd(_) | EventMsg::TurnDiff(_) => {} } } From b90c15abc42f4a84f9c37482d3c937642a808104 Mon Sep 17 00:00:00 2001 From: ae Date: Tue, 5 Aug 2025 22:01:34 -0700 Subject: [PATCH 0031/1309] clear terminal on launch (#1870) --- codex-rs/tui/src/tui.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/codex-rs/tui/src/tui.rs b/codex-rs/tui/src/tui.rs index 268483cbcf..e4f85363ad 100644 --- a/codex-rs/tui/src/tui.rs +++ b/codex-rs/tui/src/tui.rs @@ -3,11 +3,14 @@ use std::io::Stdout; use std::io::stdout; use codex_core::config::Config; +use crossterm::cursor::MoveTo; use crossterm::event::DisableBracketedPaste; use crossterm::event::EnableBracketedPaste; use crossterm::event::KeyboardEnhancementFlags; use crossterm::event::PopKeyboardEnhancementFlags; use crossterm::event::PushKeyboardEnhancementFlags; +use crossterm::terminal::Clear; +use crossterm::terminal::ClearType; use ratatui::backend::CrosstermBackend; use ratatui::crossterm::execute; use ratatui::crossterm::terminal::disable_raw_mode; @@ -36,6 +39,12 @@ pub fn init(_config: &Config) -> Result { )?; set_panic_hook(); + // Ensure the UI starts at the top of the terminal by clearing the + // current screen and moving the cursor to (0, 0) before creating the + // Terminal. This makes the initial welcome message render at the very top + // of the viewport, while keeping the normal scrollback history intact. + execute!(stdout(), Clear(ClearType::All), MoveTo(0, 0))?; + let backend = CrosstermBackend::new(stdout()); let tui = Terminal::with_options(backend)?; Ok(tui) From 966d957faf364ebd30df1db991081f9b039bbab4 Mon Sep 17 00:00:00 2001 From: easong-openai Date: Tue, 5 Aug 2025 22:34:14 -0700 Subject: [PATCH 0032/1309] fixes no git repo warning (#1863) Fix broken git warning broken-screen --- codex-rs/tui/src/app.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 1142bd87fc..da5410d392 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -233,7 +233,8 @@ impl App<'_> { widget.on_ctrl_c(); } AppState::GitWarning { .. } => { - // No-op. + // Allow exiting the app with Ctrl+C from the warning screen. + self.app_event_tx.send(AppEvent::ExitRequest); } } } @@ -415,7 +416,7 @@ impl App<'_> { let size = terminal.size()?; let desired_height = match &self.app_state { AppState::Chat { widget } => widget.desired_height(size.width), - AppState::GitWarning { .. } => 10, + AppState::GitWarning { .. } => size.height, }; let mut area = terminal.viewport_area; From f8d70d67b6d6d78c91498a34d4c2d6e40fd8bc88 Mon Sep 17 00:00:00 2001 From: easong-openai Date: Tue, 5 Aug 2025 22:35:00 -0700 Subject: [PATCH 0033/1309] Add OSS model info (#1860) Add somewhat arbitrarily chosen context window/output limit. --- codex-rs/core/src/openai_model_info.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/codex-rs/core/src/openai_model_info.rs b/codex-rs/core/src/openai_model_info.rs index 51f028cbdd..935eb8be4f 100644 --- a/codex-rs/core/src/openai_model_info.rs +++ b/codex-rs/core/src/openai_model_info.rs @@ -14,10 +14,19 @@ pub(crate) struct ModelInfo { pub(crate) max_output_tokens: u64, } -/// Note details such as what a model like gpt-4o is aliased to may be out of -/// date. pub(crate) fn get_model_info(model_family: &ModelFamily) -> Option { match model_family.slug.as_str() { + // OSS models have a 128k shared token pool. + // Arbitrarily splitting it: 3/4 input context, 1/4 output. + // https://openai.com/index/gpt-oss-model-card/ + "gpt-oss-20b" => Some(ModelInfo { + context_window: 96_000, + max_output_tokens: 32_000, + }), + "gpt-oss-120b" => Some(ModelInfo { + context_window: 96_000, + max_output_tokens: 32_000, + }), // https://platform.openai.com/docs/models/o3 "o3" => Some(ModelInfo { context_window: 200_000, From eaf2fb5b4f76f62ede52af9053d827c1959b8c68 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 5 Aug 2025 22:44:27 -0700 Subject: [PATCH 0034/1309] fix: fully enumerate EventMsg in chatwidget.rs (#1866) https://github.com/openai/codex/pull/1868 is a related fix that was in flight simultaenously, but after talking to @easong-openai, this: - logs instead of renders for `BackgroundEvent` - logs for `TurnDiff` - renders for `PatchApplyEnd` --- codex-rs/tui/src/chatwidget.rs | 17 ++++--- codex-rs/tui/src/history_cell.rs | 80 ++++++++++++++++++++++++++------ 2 files changed, 78 insertions(+), 19 deletions(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 7751404f7d..86bf765f2c 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -12,6 +12,7 @@ use codex_core::protocol::AgentReasoningEvent; use codex_core::protocol::AgentReasoningRawContentDeltaEvent; use codex_core::protocol::AgentReasoningRawContentEvent; use codex_core::protocol::ApplyPatchApprovalRequestEvent; +use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::ErrorEvent; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; @@ -25,6 +26,7 @@ use codex_core::protocol::Op; use codex_core::protocol::PatchApplyBeginEvent; use codex_core::protocol::TaskCompleteEvent; use codex_core::protocol::TokenUsage; +use codex_core::protocol::TurnDiffEvent; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; use ratatui::buffer::Buffer; @@ -33,6 +35,7 @@ use ratatui::widgets::Widget; use ratatui::widgets::WidgetRef; use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::unbounded_channel; +use tracing::info; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; @@ -435,6 +438,9 @@ impl ChatWidget<'_> { changes, )); } + EventMsg::PatchApplyEnd(patch_apply_end_event) => { + self.add_to_history(HistoryCell::new_patch_end_event(patch_apply_end_event)); + } EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id, exit_code, @@ -492,13 +498,12 @@ impl ChatWidget<'_> { EventMsg::ShutdownComplete => { self.app_event_tx.send(AppEvent::ExitRequest); } - EventMsg::BackgroundEvent(event) => { - let message = event.message; - self.add_to_history(HistoryCell::new_background_event(message.clone())); - self.update_latest_log(message); + EventMsg::TurnDiff(TurnDiffEvent { unified_diff }) => { + info!("TurnDiffEvent: {unified_diff}"); + } + EventMsg::BackgroundEvent(BackgroundEventEvent { message }) => { + info!("BackgroundEvent: {message}"); } - // TODO: Think of how are we going to render these events. - EventMsg::PatchApplyEnd(_) | EventMsg::TurnDiff(_) => {} } } diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 2fb0eecb28..332d90a647 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -12,6 +12,7 @@ use codex_core::plan_tool::StepStatus; use codex_core::plan_tool::UpdatePlanArgs; use codex_core::protocol::FileChange; use codex_core::protocol::McpInvocation; +use codex_core::protocol::PatchApplyEndEvent; use codex_core::protocol::SessionConfiguredEvent; use image::DynamicImage; use image::ImageReader; @@ -61,23 +62,35 @@ fn line_to_static(line: &Line) -> Line<'static> { /// scrollable list. pub(crate) enum HistoryCell { /// Welcome message. - WelcomeMessage { view: TextBlock }, + WelcomeMessage { + view: TextBlock, + }, /// Message from the user. - UserPrompt { view: TextBlock }, + UserPrompt { + view: TextBlock, + }, // AgentMessage and AgentReasoning variants were unused and have been removed. /// An exec tool call that has not finished yet. - ActiveExecCommand { view: TextBlock }, + ActiveExecCommand { + view: TextBlock, + }, /// Completed exec tool call. - CompletedExecCommand { view: TextBlock }, + CompletedExecCommand { + view: TextBlock, + }, /// An MCP tool call that has not finished yet. - ActiveMcpToolCall { view: TextBlock }, + ActiveMcpToolCall { + view: TextBlock, + }, /// Completed MCP tool call where we show the result serialized as JSON. - CompletedMcpToolCall { view: TextBlock }, + CompletedMcpToolCall { + view: TextBlock, + }, /// Completed MCP tool call where the result is an image. /// Admittedly, [mcp_types::CallToolResult] can have multiple content types, @@ -87,28 +100,46 @@ pub(crate) enum HistoryCell { // resized version avoids doing the potentially expensive rescale twice // because the scroll-view first calls `height()` for layouting and then // `render_window()` for painting. - CompletedMcpToolCallWithImageOutput { _image: DynamicImage }, + CompletedMcpToolCallWithImageOutput { + _image: DynamicImage, + }, /// Background event. - BackgroundEvent { view: TextBlock }, + BackgroundEvent { + view: TextBlock, + }, /// Output from the `/diff` command. - GitDiffOutput { view: TextBlock }, + GitDiffOutput { + view: TextBlock, + }, /// Error event from the backend. - ErrorEvent { view: TextBlock }, + ErrorEvent { + view: TextBlock, + }, /// Info describing the newly-initialized session. - SessionInfo { view: TextBlock }, + SessionInfo { + view: TextBlock, + }, /// A pending code patch that is awaiting user approval. Mirrors the /// behaviour of `ActiveExecCommand` so the user sees *what* patch the /// model wants to apply before being prompted to approve or deny it. - PendingPatch { view: TextBlock }, + PendingPatch { + view: TextBlock, + }, + + PatchEventEnd { + view: TextBlock, + }, /// A human‑friendly rendering of the model's current plan and step /// statuses provided via the `update_plan` tool. - PlanUpdate { view: TextBlock }, + PlanUpdate { + view: TextBlock, + }, } const TOOL_CALL_MAX_LINES: usize = 5; @@ -128,6 +159,7 @@ impl HistoryCell { | HistoryCell::CompletedExecCommand { view } | HistoryCell::CompletedMcpToolCall { view } | HistoryCell::PendingPatch { view } + | HistoryCell::PatchEventEnd { view } | HistoryCell::PlanUpdate { view } | HistoryCell::ActiveExecCommand { view, .. } | HistoryCell::ActiveMcpToolCall { view, .. } => { @@ -598,6 +630,28 @@ impl HistoryCell { view: TextBlock::new(lines), } } + + pub(crate) fn new_patch_end_event(patch_apply_end_event: PatchApplyEndEvent) -> Self { + let PatchApplyEndEvent { + call_id: _, + stdout: _, + stderr, + success, + } = patch_apply_end_event; + + let mut lines: Vec> = if success { + vec![Line::from("patch applied successfully".italic())] + } else { + let mut lines = vec![Line::from("patch failed".italic())]; + lines.extend(stderr.lines().map(|l| Line::from(l.to_string()))); + lines + }; + lines.push(Line::from("")); + + HistoryCell::PatchEventEnd { + view: TextBlock::new(lines), + } + } } fn create_diff_summary(changes: HashMap) -> Vec { From 1f7003b47633e56a9e8fa50fa4f277f338f36a00 Mon Sep 17 00:00:00 2001 From: ae Date: Tue, 5 Aug 2025 23:02:00 -0700 Subject: [PATCH 0035/1309] tweak comment (#1871) Belatedly address CR feedback about a comment. ------ https://chatgpt.com/codex/tasks/task_i_6892e8070be4832cba379f2955f5b8bc --- codex-rs/tui/src/tui.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/codex-rs/tui/src/tui.rs b/codex-rs/tui/src/tui.rs index e4f85363ad..e0bf9bcc57 100644 --- a/codex-rs/tui/src/tui.rs +++ b/codex-rs/tui/src/tui.rs @@ -39,10 +39,7 @@ pub fn init(_config: &Config) -> Result { )?; set_panic_hook(); - // Ensure the UI starts at the top of the terminal by clearing the - // current screen and moving the cursor to (0, 0) before creating the - // Terminal. This makes the initial welcome message render at the very top - // of the viewport, while keeping the normal scrollback history intact. + // Clear screen and move cursor to top-left before drawing UI execute!(stdout(), Clear(ClearType::All), MoveTo(0, 0))?; let backend = CrosstermBackend::new(stdout()); From 493e4c94631b7322f43cc08d898469f20c0902b4 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 5 Aug 2025 23:11:29 -0700 Subject: [PATCH 0036/1309] fix: only tag as prerelease when the version has an -alpha or -beta suffix (#1872) Hardcoding to `prerelease: true` is a holdover from before we had migrated to the Rust CLI for releases and decided on how we were doing version numbers. To date, I have had to change the release status from "prerelease" to "actual release" manually through the GitHub Releases web page. This is a semi-serious problem because I've discovered that it messes up Homebrew's automation if the version number _looks_ like a real release but turns out to be a prerelease. The release potentially gets skipped from being published on Homebrew, so it's important to set the value correctly from the start. I verified that `steps.release_name.outputs.name` does not include the `rust-v` prefix from the tag name. --- .github/workflows/rust-release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 3f1c084d91..812d7a3cfe 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -181,9 +181,9 @@ jobs: name: ${{ steps.release_name.outputs.name }} tag_name: ${{ github.ref_name }} files: dist/** - # For now, tag releases as "prerelease" because we are not claiming - # the Rust CLI is stable yet. - prerelease: true + # Mark as prerelease only when the version has a suffix after x.y.z + # (e.g. -alpha, -beta). Otherwise publish a normal release. + prerelease: ${{ contains(steps.release_name.outputs.name, '-') }} - uses: facebook/dotslash-publish-release@v2 env: From 02e796522869071d658f899356608a242e2f85e7 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 5 Aug 2025 23:33:21 -0700 Subject: [PATCH 0037/1309] fix: add stricter checks and better error messages to create_github_release.sh (#1874) This script attempts to verify that: - You have no local, uncommitted changes. - You are on `main` - The commit you are on exists on `main` also exists on the origin `https://github.com/openai/codex`, i.e., it is not just a commit you have pushed to your local version of `main` As part of this, try to print better error message if/when these conditions are violated. --- codex-rs/scripts/create_github_release.sh | 28 ++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/codex-rs/scripts/create_github_release.sh b/codex-rs/scripts/create_github_release.sh index 84dcb95fa0..4bc4e25125 100755 --- a/codex-rs/scripts/create_github_release.sh +++ b/codex-rs/scripts/create_github_release.sh @@ -19,7 +19,33 @@ if ! git diff --quiet || ! git diff --cached --quiet || [ -n "$(git ls-files --o fi # Fail if in a detached HEAD state. -CURRENT_BRANCH=$(git symbolic-ref --short -q HEAD) +CURRENT_BRANCH=$(git symbolic-ref --short -q HEAD 2>/dev/null || true) +if [ -z "${CURRENT_BRANCH:-}" ]; then + echo "ERROR: Could not determine the current branch (detached HEAD?)." >&2 + echo " Please run this script from a checked-out branch." >&2 + exit 1 +fi + +# Ensure we are on the 'main' branch before proceeding. +if [ "${CURRENT_BRANCH}" != "main" ]; then + echo "ERROR: Releases must be created from the 'main' branch (current: '${CURRENT_BRANCH}')." >&2 + echo " Please switch to 'main' and try again." >&2 + exit 1 +fi + +# Ensure the current local commit on 'main' is present on 'origin/main'. +# This guarantees we only create releases from commits that are already on +# the canonical repository (https://github.com/openai/codex). +if ! git fetch --quiet origin main; then + echo "ERROR: Failed to fetch 'origin/main'. Ensure the 'origin' remote is configured and reachable." >&2 + exit 1 +fi + +if ! git merge-base --is-ancestor HEAD origin/main; then + echo "ERROR: Your local 'main' HEAD commit is not present on 'origin/main'." >&2 + echo " Please push your commits first (git push origin main) or check out a commit on 'origin/main'." >&2 + exit 1 +fi # Create a new branch for the release and make a commit with the new version. if [ $# -ge 1 ]; then From 7b3ab968a0ee599b4eae03956322caac1642f55c Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 5 Aug 2025 23:36:10 -0700 Subject: [PATCH 0038/1309] docs: add more detail to the codex-rust-review (#1875) This PR attempts to break `codex-rust-review.md` into sections so that it is easier to consume. It also adds a healthy new section on "Assertions in Tests" that has been on my mind for awhile. --- .github/codex/labels/codex-rust-review.md | 128 +++++++++++++++++++++- 1 file changed, 122 insertions(+), 6 deletions(-) diff --git a/.github/codex/labels/codex-rust-review.md b/.github/codex/labels/codex-rust-review.md index 2c2893a1fe..ae82d272d7 100644 --- a/.github/codex/labels/codex-rust-review.md +++ b/.github/codex/labels/codex-rust-review.md @@ -6,18 +6,134 @@ Then provide the **review** (1-2 sentences plus bullet points, friendly tone). Things to look out for when doing the review: +## General Principles + - **Make sure the pull request body explains the motivation behind the change.** If the author has failed to do this, call it out, and if you think you can deduce the motivation behind the change, propose copy. - Ideally, the PR body also contains a small summary of the change. For small changes, the PR title may be sufficient. - Each PR should ideally do one conceptual thing. For example, if a PR does a refactoring as well as introducing a new feature, push back and suggest the refactoring be done in a separate PR. This makes things easier for the reviewer, as refactoring changes can often be far-reaching, yet quick to review. -- If the nature of the change seems to have a visual component (which is often the case for changes to `codex-rs/tui`), recommend including a screenshot or video to demonstrate the change, if appropriate. -- Rust files should generally be organized such that the public parts of the API appear near the top of the file and helper functions go below. This is analagous to the "inverted pyramid" structure that is favored in journalism. -- Encourage the use of small enums or the newtype pattern in Rust if it helps readability without adding significant cognitive load or lines of code. -- Be wary of large files and offer suggestions for how to break things into more reasonably-sized files. -- When modifying a `Cargo.toml` file, make sure that dependency lists stay alphabetically sorted. Also consider whether a new dependency is added to the appropriate place (e.g., `[dependencies]` versus `[dev-dependencies]`) -- If you see opportunities for the changes in a diff to use more idiomatic Rust, please make specific recommendations. For example, favor the use of expressions over `return`. - When introducing new code, be on the lookout for code that duplicates existing code. When found, propose a way to refactor the existing code such that it should be reused. + +## Code Organization + - Each create in the Cargo workspace in `codex-rs` has a specific purpose: make a note if you believe new code is not introduced in the correct crate. - When possible, try to keep the `core` crate as small as possible. Non-core but shared logic is often a good candidate for `codex-rs/common`. +- Be wary of large files and offer suggestions for how to break things into more reasonably-sized files. +- Rust files should generally be organized such that the public parts of the API appear near the top of the file and helper functions go below. This is analagous to the "inverted pyramid" structure that is favored in journalism. + +## Assertions in Tests + +Assert the equality of the entire objects instead of doing "piecemeal comparisons," performing `assert_eq!()` on individual fields. + +Note that unit tests also function as "executable documentation." As shown in the following example, "piecemeal comparisons" are often more verbose, provide less coverage, and are not as useful as executable documentation. + +For example, suppose you have the following enum: + +```rust +#[derive(Debug, PartialEq)] +enum Message { + Request { + id: String, + method: String, + params: Option, + }, + Notification { + method: String, + params: Option, + }, +} +``` + +This is an example of a _piecemeal_ comparison: + +```rust +// BAD: Piecemeal Comparison + +#[test] +fn test_get_latest_messages() { + let messages = get_latest_messages(); + assert_eq!(messages.len(), 2); + + let m0 = &messages[0]; + match m0 { + Message::Request { id, method, params } => { + assert_eq!(id, "123"); + assert_eq!(method, "subscribe"); + assert_eq!( + *params, + Some(json!({ + "conversation_id": "x42z86" + })) + ) + } + Message::Notification { .. } => { + panic!("expected Request"); + } + } + + let m1 = &messages[1]; + match m1 { + Message::Request { .. } => { + panic!("expected Notification"); + } + Message::Notification { method, params } => { + assert_eq!(method, "log"); + assert_eq!( + *params, + Some(json!({ + "level": "info", + "message": "subscribed" + })) + ) + } + } +} +``` + +This is a _deep_ comparison: + +```rust +// GOOD: Verify the entire structure with a single assert_eq!(). + +use pretty_assertions::assert_eq; + +#[test] +fn test_get_latest_messages() { + let messages = get_latest_messages(); + + assert_eq!( + vec![ + Message::Request { + id: "123".to_string(), + method: "subscribe".to_string(), + params: Some(json!({ + "conversation_id": "x42z86" + })), + }, + Message::Notification { + method: "log".to_string(), + params: Some(json!({ + "level": "info", + "message": "subscribed" + })), + }, + ], + messages, + ); +} +``` + +## More Tactical Rust Things To Look Out For + +- Do not use `unsafe` (unless you have a really, really good reason like using an operating system API directly and no safe wrapper exists). For example, there are cases where it is tempting to use `unsafe` in order to use `std::env::set_var()`, but this indeed `unsafe` and has led to race conditions on multiple occasions. (When this happens, find a mechanism other than environment variables to use for configuration.) +- Encourage the use of small enums or the newtype pattern in Rust if it helps readability without adding significant cognitive load or lines of code. +- If you see opportunities for the changes in a diff to use more idiomatic Rust, please make specific recommendations. For example, favor the use of expressions over `return`. +- When modifying a `Cargo.toml` file, make sure that dependency lists stay alphabetically sorted. Also consider whether a new dependency is added to the appropriate place (e.g., `[dependencies]` versus `[dev-dependencies]`) + +## Pull Request Body + +- If the nature of the change seems to have a visual component (which is often the case for changes to `codex-rs/tui`), recommend including a screenshot or video to demonstrate the change, if appropriate. - References to existing GitHub issues and PRs are encouraged, where appropriate, though you likely do not have network access, so may not be able to help here. +# PR Information + {CODEX_ACTION_GITHUB_EVENT_PATH} contains the JSON that triggered this GitHub workflow. It contains the `base` and `head` refs that define this PR. Both refs are available locally. From d642b07fcccdd8554316c61b88743378db43252c Mon Sep 17 00:00:00 2001 From: ae Date: Tue, 5 Aug 2025 23:57:52 -0700 Subject: [PATCH 0039/1309] [feat] add /status slash command (#1873) - Added a `/status` command, which will be useful when we update the home screen to print less status. - Moved `create_config_summary_entries` to common since it's used in a few places. - Noticed we inconsistently had periods in slash command descriptions and just removed them everywhere. - Noticed the diff description was overflowing so made it shorter. --- codex-rs/common/src/config_summary.rs | 29 ++++++++ codex-rs/common/src/lib.rs | 4 + codex-rs/exec/src/event_processor.rs | 26 ------- .../src/event_processor_with_human_output.rs | 2 +- .../src/event_processor_with_json_output.rs | 2 +- codex-rs/tui/src/app.rs | 5 ++ codex-rs/tui/src/chatwidget.rs | 7 ++ codex-rs/tui/src/history_cell.rs | 74 +++++++++++++------ codex-rs/tui/src/slash_command.rs | 12 +-- 9 files changed, 105 insertions(+), 56 deletions(-) create mode 100644 codex-rs/common/src/config_summary.rs diff --git a/codex-rs/common/src/config_summary.rs b/codex-rs/common/src/config_summary.rs new file mode 100644 index 0000000000..39d524731f --- /dev/null +++ b/codex-rs/common/src/config_summary.rs @@ -0,0 +1,29 @@ +use codex_core::WireApi; +use codex_core::config::Config; + +use crate::sandbox_summary::summarize_sandbox_policy; + +/// Build a list of key/value pairs summarizing the effective configuration. +pub fn create_config_summary_entries(config: &Config) -> Vec<(&'static str, String)> { + let mut entries = vec![ + ("workdir", config.cwd.display().to_string()), + ("model", config.model.clone()), + ("provider", config.model_provider_id.clone()), + ("approval", config.approval_policy.to_string()), + ("sandbox", summarize_sandbox_policy(&config.sandbox_policy)), + ]; + if config.model_provider.wire_api == WireApi::Responses + && config.model_family.supports_reasoning_summaries + { + entries.push(( + "reasoning effort", + config.model_reasoning_effort.to_string(), + )); + entries.push(( + "reasoning summaries", + config.model_reasoning_summary.to_string(), + )); + } + + entries +} diff --git a/codex-rs/common/src/lib.rs b/codex-rs/common/src/lib.rs index 3d498a8e2c..38f3832bfd 100644 --- a/codex-rs/common/src/lib.rs +++ b/codex-rs/common/src/lib.rs @@ -23,3 +23,7 @@ mod sandbox_summary; #[cfg(feature = "sandbox_summary")] pub use sandbox_summary::summarize_sandbox_policy; + +mod config_summary; + +pub use config_summary::create_config_summary_entries; diff --git a/codex-rs/exec/src/event_processor.rs b/codex-rs/exec/src/event_processor.rs index 0a2a141eca..b7b3c27dc5 100644 --- a/codex-rs/exec/src/event_processor.rs +++ b/codex-rs/exec/src/event_processor.rs @@ -1,7 +1,5 @@ use std::path::Path; -use codex_common::summarize_sandbox_policy; -use codex_core::WireApi; use codex_core::config::Config; use codex_core::protocol::Event; @@ -19,30 +17,6 @@ pub(crate) trait EventProcessor { fn process_event(&mut self, event: Event) -> CodexStatus; } -pub(crate) fn create_config_summary_entries(config: &Config) -> Vec<(&'static str, String)> { - let mut entries = vec![ - ("workdir", config.cwd.display().to_string()), - ("model", config.model.clone()), - ("provider", config.model_provider_id.clone()), - ("approval", config.approval_policy.to_string()), - ("sandbox", summarize_sandbox_policy(&config.sandbox_policy)), - ]; - if config.model_provider.wire_api == WireApi::Responses - && config.model_family.supports_reasoning_summaries - { - entries.push(( - "reasoning effort", - config.model_reasoning_effort.to_string(), - )); - entries.push(( - "reasoning summaries", - config.model_reasoning_summary.to_string(), - )); - } - - entries -} - pub(crate) fn handle_last_message(last_agent_message: Option<&str>, output_file: &Path) { let message = last_agent_message.unwrap_or_default(); write_last_message_file(message, Some(output_file)); diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 393ef4ab1b..6b03ed7882 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -33,8 +33,8 @@ use std::time::Instant; use crate::event_processor::CodexStatus; use crate::event_processor::EventProcessor; -use crate::event_processor::create_config_summary_entries; use crate::event_processor::handle_last_message; +use codex_common::create_config_summary_entries; /// This should be configurable. When used in CI, users may not want to impose /// a limit so they can see the full transcript. diff --git a/codex-rs/exec/src/event_processor_with_json_output.rs b/codex-rs/exec/src/event_processor_with_json_output.rs index 1d153add6e..76985518e6 100644 --- a/codex-rs/exec/src/event_processor_with_json_output.rs +++ b/codex-rs/exec/src/event_processor_with_json_output.rs @@ -9,8 +9,8 @@ use serde_json::json; use crate::event_processor::CodexStatus; use crate::event_processor::EventProcessor; -use crate::event_processor::create_config_summary_entries; use crate::event_processor::handle_last_message; +use codex_common::create_config_summary_entries; pub(crate) struct EventProcessorWithJsonOutput { last_message_path: Option, diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index da5410d392..eee2a61c2e 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -330,6 +330,11 @@ impl App<'_> { widget.add_diff_output(text); } } + SlashCommand::Status => { + if let AppState::Chat { widget } = &mut self.app_state { + widget.add_status_output(); + } + } #[cfg(debug_assertions)] SlashCommand::TestApproval => { use std::collections::HashMap; diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 86bf765f2c..6d03be783b 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -522,6 +522,13 @@ impl ChatWidget<'_> { self.add_to_history(HistoryCell::new_diff_output(diff_output.clone())); } + pub(crate) fn add_status_output(&mut self) { + self.add_to_history(HistoryCell::new_status_output( + &self.config, + &self.token_usage, + )); + } + /// Forward file-search results to the bottom pane. pub(crate) fn apply_file_search_result(&mut self, query: String, matches: Vec) { self.bottom_pane.on_file_search_result(query, matches); diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 332d90a647..5b7d9246f7 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -3,9 +3,8 @@ use crate::text_block::TextBlock; use crate::text_formatting::format_and_truncate_tool_result; use base64::Engine; use codex_ansi_escape::ansi_escape_line; +use codex_common::create_config_summary_entries; use codex_common::elapsed::format_duration; -use codex_common::summarize_sandbox_policy; -use codex_core::WireApi; use codex_core::config::Config; use codex_core::plan_tool::PlanItemArg; use codex_core::plan_tool::StepStatus; @@ -14,6 +13,7 @@ use codex_core::protocol::FileChange; use codex_core::protocol::McpInvocation; use codex_core::protocol::PatchApplyEndEvent; use codex_core::protocol::SessionConfiguredEvent; +use codex_core::protocol::TokenUsage; use image::DynamicImage; use image::ImageReader; use mcp_types::EmbeddedResourceResource; @@ -114,6 +114,11 @@ pub(crate) enum HistoryCell { view: TextBlock, }, + /// Output from the `/status` command. + StatusOutput { + view: TextBlock, + }, + /// Error event from the backend. ErrorEvent { view: TextBlock, @@ -154,6 +159,7 @@ impl HistoryCell { | HistoryCell::UserPrompt { view } | HistoryCell::BackgroundEvent { view } | HistoryCell::GitDiffOutput { view } + | HistoryCell::StatusOutput { view } | HistoryCell::ErrorEvent { view } | HistoryCell::SessionInfo { view } | HistoryCell::CompletedExecCommand { view } @@ -200,26 +206,7 @@ impl HistoryCell { ]), ]; - let mut entries = vec![ - ("workdir", config.cwd.display().to_string()), - ("model", config.model.clone()), - ("provider", config.model_provider_id.clone()), - ("approval", config.approval_policy.to_string()), - ("sandbox", summarize_sandbox_policy(&config.sandbox_policy)), - ]; - if config.model_provider.wire_api == WireApi::Responses - && config.model_family.supports_reasoning_summaries - { - entries.push(( - "reasoning effort", - config.model_reasoning_effort.to_string(), - )); - entries.push(( - "reasoning summaries", - config.model_reasoning_summary.to_string(), - )); - } - for (key, value) in entries { + for (key, value) in create_config_summary_entries(config) { lines.push(Line::from(vec![format!("{key}: ").bold(), value.into()])); } lines.push(Line::from("")); @@ -476,6 +463,49 @@ impl HistoryCell { } } + pub(crate) fn new_status_output(config: &Config, usage: &TokenUsage) -> Self { + let mut lines: Vec> = Vec::new(); + lines.push(Line::from("/status".magenta())); + + // Config + for (key, value) in create_config_summary_entries(config) { + lines.push(Line::from(vec![format!("{key}: ").bold(), value.into()])); + } + + // Token usage + lines.push(Line::from("")); + lines.push(Line::from("token usage".bold())); + lines.push(Line::from(vec![ + " input: ".bold(), + usage.input_tokens.to_string().into(), + ])); + lines.push(Line::from(vec![ + " cached input: ".bold(), + usage.cached_input_tokens.unwrap_or(0).to_string().into(), + ])); + lines.push(Line::from(vec![ + " output: ".bold(), + usage.output_tokens.to_string().into(), + ])); + lines.push(Line::from(vec![ + " reasoning output: ".bold(), + usage + .reasoning_output_tokens + .unwrap_or(0) + .to_string() + .into(), + ])); + lines.push(Line::from(vec![ + " total: ".bold(), + usage.total_tokens.to_string().into(), + ])); + + lines.push(Line::from("")); + HistoryCell::StatusOutput { + view: TextBlock::new(lines), + } + } + pub(crate) fn new_error_event(message: String) -> Self { let lines: Vec> = vec![ vec!["ERROR: ".red().bold(), message.into()].into(), diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index d82a16608f..85dde7a113 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -15,6 +15,7 @@ pub enum SlashCommand { New, Compact, Diff, + Status, Quit, #[cfg(debug_assertions)] TestApproval, @@ -24,12 +25,11 @@ impl SlashCommand { /// User-visible description shown in the popup. pub fn description(self) -> &'static str { match self { - SlashCommand::New => "Start a new chat.", - SlashCommand::Compact => "Compact the chat history.", - SlashCommand::Quit => "Exit the application.", - SlashCommand::Diff => { - "Show git diff of the working directory (including untracked files)" - } + SlashCommand::New => "Start a new chat", + SlashCommand::Compact => "Compact the chat history", + SlashCommand::Quit => "Exit the application", + SlashCommand::Diff => "Show git diff (including untracked files)", + SlashCommand::Status => "Show current session configuration and token usage", #[cfg(debug_assertions)] SlashCommand::TestApproval => "Test approval request", } From cda39e417fb3c4d91f02ccc93acc296b77f5b947 Mon Sep 17 00:00:00 2001 From: Dylan Date: Wed, 6 Aug 2025 00:07:58 -0700 Subject: [PATCH 0040/1309] [tests] Investigate flakey mcp-server test (#1877) ## Summary Have seen these tests flaking over the course of today on different boxes. `wiremock` seems to be generally written with tokio/threads in mind but based on the weird panics from the tests, let's see if this helps. --- codex-rs/mcp-server/tests/send_message.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codex-rs/mcp-server/tests/send_message.rs b/codex-rs/mcp-server/tests/send_message.rs index fd4b210b0b..fd3718e8f3 100644 --- a/codex-rs/mcp-server/tests/send_message.rs +++ b/codex-rs/mcp-server/tests/send_message.rs @@ -18,7 +18,7 @@ use tokio::time::timeout; const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[tokio::test] async fn test_send_message_success() { // Spin up a mock completions server that immediately ends the Codex turn. // Two Codex turns hit the mock model (session start + send-user-message). Provide two SSE responses. @@ -105,7 +105,7 @@ async fn test_send_message_success() { drop(server); } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[tokio::test] async fn test_send_message_session_not_found() { // Start MCP without creating a Codex session let codex_home = TempDir::new().expect("tempdir"); From 3e8bcf0247ee37a226dd3c8b1e7e6695ccb9321b Mon Sep 17 00:00:00 2001 From: Dylan Date: Wed, 6 Aug 2025 01:13:31 -0700 Subject: [PATCH 0041/1309] [prompts] Add (#1869) ## Summary Includes a new user message in the api payload which provides useful environment context for the model, so it knows about things like the current working directory and the sandbox. ## Testing Updated unit tests --- codex-rs/core/src/chat_completions.rs | 6 +- codex-rs/core/src/client.rs | 11 +--- codex-rs/core/src/client_common.rs | 77 ++++++++++++++++++++++- codex-rs/core/src/codex.rs | 9 +++ codex-rs/core/src/git_info.rs | 2 +- codex-rs/core/src/protocol.rs | 3 +- codex-rs/core/tests/client.rs | 47 ++++++++++---- codex-rs/mcp-server/tests/send_message.rs | 2 +- 8 files changed, 126 insertions(+), 31 deletions(-) diff --git a/codex-rs/core/src/chat_completions.rs b/codex-rs/core/src/chat_completions.rs index 98ef7f26cc..dae140bc02 100644 --- a/codex-rs/core/src/chat_completions.rs +++ b/codex-rs/core/src/chat_completions.rs @@ -41,11 +41,9 @@ pub(crate) async fn stream_chat_completions( let full_instructions = prompt.get_full_instructions(model_family); messages.push(json!({"role": "system", "content": full_instructions})); - if let Some(instr) = &prompt.get_formatted_user_instructions() { - messages.push(json!({"role": "user", "content": instr})); - } + let input = prompt.get_formatted_input(); - for item in &prompt.input { + for item in &input { match item { ResponseItem::Message { role, content, .. } => { let mut text = String::new(); diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index e4bb30da26..9748cde7cb 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -34,7 +34,6 @@ use crate::error::Result; use crate::flags::CODEX_RS_SSE_FIXTURE; use crate::model_provider_info::ModelProviderInfo; use crate::model_provider_info::WireApi; -use crate::models::ContentItem; use crate::models::ResponseItem; use crate::openai_tools::create_tools_json_for_responses_api; use crate::protocol::TokenUsage; @@ -146,15 +145,7 @@ impl ModelClient { vec![] }; - let mut input_with_instructions = Vec::with_capacity(prompt.input.len() + 1); - if let Some(ui) = prompt.get_formatted_user_instructions() { - input_with_instructions.push(ResponseItem::Message { - id: None, - role: "user".to_string(), - content: vec![ContentItem::InputText { text: ui }], - }); - } - input_with_instructions.extend(prompt.input.clone()); + let input_with_instructions = prompt.get_formatted_input(); let payload = ResponsesApiRequest { model: &self.config.model, diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index 60164f5fde..2ca060f4a4 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -1,14 +1,20 @@ use crate::config_types::ReasoningEffort as ReasoningEffortConfig; use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; use crate::error::Result; +use crate::git_info::GitInfo; use crate::model_family::ModelFamily; +use crate::models::ContentItem; use crate::models::ResponseItem; use crate::openai_tools::OpenAiTool; +use crate::protocol::AskForApproval; +use crate::protocol::SandboxPolicy; use crate::protocol::TokenUsage; use codex_apply_patch::APPLY_PATCH_TOOL_INSTRUCTIONS; use futures::Stream; use serde::Serialize; use std::borrow::Cow; +use std::fmt::Display; +use std::path::PathBuf; use std::pin::Pin; use std::task::Context; use std::task::Poll; @@ -18,10 +24,49 @@ use tokio::sync::mpsc; /// with this content. const BASE_INSTRUCTIONS: &str = include_str!("../prompt.md"); +/// wraps environment context message in a tag for the model to parse more easily. +const ENVIRONMENT_CONTEXT_START: &str = "\n\n"; +const ENVIRONMENT_CONTEXT_END: &str = "\n\n"; + /// wraps user instructions message in a tag for the model to parse more easily. const USER_INSTRUCTIONS_START: &str = "\n\n"; const USER_INSTRUCTIONS_END: &str = "\n\n"; +#[derive(Debug, Clone)] +pub(crate) struct EnvironmentContext { + pub cwd: PathBuf, + pub git_info: Option, + pub approval_policy: AskForApproval, + pub sandbox_policy: SandboxPolicy, +} + +impl Display for EnvironmentContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!( + f, + "Current working directory: {}", + self.cwd.to_string_lossy() + )?; + writeln!(f, "Is directory a git repo: {}", self.git_info.is_some())?; + writeln!(f, "Approval policy: {}", self.approval_policy)?; + writeln!(f, "Sandbox policy: {}", self.sandbox_policy)?; + + let network_access = match self.sandbox_policy.clone() { + SandboxPolicy::DangerFullAccess => "enabled", + SandboxPolicy::ReadOnly => "restricted", + SandboxPolicy::WorkspaceWrite { network_access, .. } => { + if network_access { + "enabled" + } else { + "restricted" + } + } + }; + writeln!(f, "Network access: {network_access}")?; + Ok(()) + } +} + /// API request payload for a single model turn. #[derive(Default, Debug, Clone)] pub struct Prompt { @@ -33,6 +78,10 @@ pub struct Prompt { /// Whether to store response on server side (disable_response_storage = !store). pub store: bool, + /// A list of key-value pairs that will be added as a developer message + /// for the model to use + pub environment_context: Option, + /// Tools available to the model, including additional tools sourced from /// external MCP servers. pub tools: Vec, @@ -54,11 +103,37 @@ impl Prompt { Cow::Owned(sections.join("\n")) } - pub(crate) fn get_formatted_user_instructions(&self) -> Option { + fn get_formatted_user_instructions(&self) -> Option { self.user_instructions .as_ref() .map(|ui| format!("{USER_INSTRUCTIONS_START}{ui}{USER_INSTRUCTIONS_END}")) } + + fn get_formatted_environment_context(&self) -> Option { + self.environment_context + .as_ref() + .map(|ec| format!("{ENVIRONMENT_CONTEXT_START}{ec}{ENVIRONMENT_CONTEXT_END}")) + } + + pub(crate) fn get_formatted_input(&self) -> Vec { + let mut input_with_instructions = Vec::with_capacity(self.input.len() + 2); + if let Some(ec) = self.get_formatted_environment_context() { + input_with_instructions.push(ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { text: ec }], + }); + } + if let Some(ui) = self.get_formatted_user_instructions() { + input_with_instructions.push(ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { text: ui }], + }); + } + input_with_instructions.extend(self.input.clone()); + input_with_instructions + } } #[derive(Debug)] diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index a7ab664ee0..c85b1ce2b9 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -37,6 +37,7 @@ use crate::apply_patch::convert_apply_patch_to_protocol; use crate::apply_patch::get_writable_roots; use crate::apply_patch::{self}; use crate::client::ModelClient; +use crate::client_common::EnvironmentContext; use crate::client_common::Prompt; use crate::client_common::ResponseEvent; use crate::config::Config; @@ -51,6 +52,7 @@ use crate::exec::SandboxType; use crate::exec::StdoutStream; use crate::exec::process_exec_tool_call; use crate::exec_env::create_env; +use crate::git_info::collect_git_info; use crate::mcp_connection_manager::McpConnectionManager; use crate::mcp_tool_call::handle_mcp_tool_call; use crate::models::ContentItem; @@ -1224,6 +1226,12 @@ async fn run_turn( store: !sess.disable_response_storage, tools, base_instructions_override: sess.base_instructions.clone(), + environment_context: Some(EnvironmentContext { + cwd: sess.cwd.clone(), + git_info: collect_git_info(&sess.cwd).await, + approval_policy: sess.approval_policy, + sandbox_policy: sess.sandbox_policy.clone(), + }), }; let mut retries = 0; @@ -1449,6 +1457,7 @@ async fn run_compact_task( input: turn_input, user_instructions: None, store: !sess.disable_response_storage, + environment_context: None, tools: Vec::new(), base_instructions_override: Some(compact_instructions.clone()), }; diff --git a/codex-rs/core/src/git_info.rs b/codex-rs/core/src/git_info.rs index f5dc016e66..52d029f669 100644 --- a/codex-rs/core/src/git_info.rs +++ b/codex-rs/core/src/git_info.rs @@ -9,7 +9,7 @@ use tokio::time::timeout; /// Timeout for git commands to prevent freezing on large repositories const GIT_COMMAND_TIMEOUT: TokioDuration = TokioDuration::from_secs(5); -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub struct GitInfo { /// Current commit hash (SHA) #[serde(skip_serializing_if = "Option::is_none")] diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs index 9bf85ec49a..55000fb6d7 100644 --- a/codex-rs/core/src/protocol.rs +++ b/codex-rs/core/src/protocol.rs @@ -159,7 +159,8 @@ pub enum AskForApproval { } /// Determines execution restrictions for model shell commands. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display)] +#[strum(serialize_all = "kebab-case")] #[serde(tag = "mode", rename_all = "kebab-case")] pub enum SandboxPolicy { /// No restrictions whatsoever. Use with caution. diff --git a/codex-rs/core/tests/client.rs b/codex-rs/core/tests/client.rs index f493020210..00f91a879e 100644 --- a/codex-rs/core/tests/client.rs +++ b/codex-rs/core/tests/client.rs @@ -1,3 +1,5 @@ +#![allow(clippy::expect_used)] +#![allow(clippy::unwrap_used)] use std::path::PathBuf; use chrono::Utc; @@ -32,6 +34,32 @@ fn sse_completed(id: &str) -> String { load_sse_fixture_with_id("tests/fixtures/completed_template.json", id) } +fn assert_message_role(request_body: &serde_json::Value, role: &str) { + assert_eq!(request_body["role"].as_str().unwrap(), role); +} + +fn assert_message_starts_with(request_body: &serde_json::Value, text: &str) { + let content = request_body["content"][0]["text"] + .as_str() + .expect("invalid message content"); + + assert!( + content.starts_with(text), + "expected message content '{content}' to start with '{text}'" + ); +} + +fn assert_message_ends_with(request_body: &serde_json::Value, text: &str) { + let content = request_body["content"][0]["text"] + .as_str() + .expect("invalid message content"); + + assert!( + content.ends_with(text), + "expected message content '{content}' to end with '{text}'" + ); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn includes_session_id_and_model_headers_in_request() { #![allow(clippy::unwrap_used)] @@ -371,19 +399,12 @@ async fn includes_user_instructions_message_in_request() { .unwrap() .contains("be nice") ); - assert_eq!(request_body["input"][0]["role"], "user"); - assert!( - request_body["input"][0]["content"][0]["text"] - .as_str() - .unwrap() - .starts_with("\n\nbe nice") - ); - assert!( - request_body["input"][0]["content"][0]["text"] - .as_str() - .unwrap() - .ends_with("") - ); + assert_message_role(&request_body["input"][0], "user"); + assert_message_starts_with(&request_body["input"][0], "\n\n"); + assert_message_ends_with(&request_body["input"][0], ""); + assert_message_role(&request_body["input"][1], "user"); + assert_message_starts_with(&request_body["input"][1], "\n\n"); + assert_message_ends_with(&request_body["input"][1], ""); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] diff --git a/codex-rs/mcp-server/tests/send_message.rs b/codex-rs/mcp-server/tests/send_message.rs index fd3718e8f3..6e1389093c 100644 --- a/codex-rs/mcp-server/tests/send_message.rs +++ b/codex-rs/mcp-server/tests/send_message.rs @@ -99,7 +99,7 @@ async fn test_send_message_success() { response ); // wait for the server to hear the user message - sleep(Duration::from_secs(1)); + sleep(Duration::from_secs(10)); // Ensure the server and tempdir live until end of test drop(server); From dc468d563f1cdf3dfbeeb97dc29543b3ae9442e2 Mon Sep 17 00:00:00 2001 From: Dylan Date: Wed, 6 Aug 2025 08:05:17 -0700 Subject: [PATCH 0042/1309] [env] Remove git config for now (#1884) ## Summary Forgot to remove this in #1869 last night! Too much of a performance hit on the main thread. We can bring it back via an async thread on startup. --- codex-rs/core/src/client_common.rs | 3 --- codex-rs/core/src/codex.rs | 2 -- codex-rs/mcp-server/tests/send_message.rs | 2 +- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index 2ca060f4a4..b37b1e3f80 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -1,7 +1,6 @@ use crate::config_types::ReasoningEffort as ReasoningEffortConfig; use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; use crate::error::Result; -use crate::git_info::GitInfo; use crate::model_family::ModelFamily; use crate::models::ContentItem; use crate::models::ResponseItem; @@ -35,7 +34,6 @@ const USER_INSTRUCTIONS_END: &str = "\n\n"; #[derive(Debug, Clone)] pub(crate) struct EnvironmentContext { pub cwd: PathBuf, - pub git_info: Option, pub approval_policy: AskForApproval, pub sandbox_policy: SandboxPolicy, } @@ -47,7 +45,6 @@ impl Display for EnvironmentContext { "Current working directory: {}", self.cwd.to_string_lossy() )?; - writeln!(f, "Is directory a git repo: {}", self.git_info.is_some())?; writeln!(f, "Approval policy: {}", self.approval_policy)?; writeln!(f, "Sandbox policy: {}", self.sandbox_policy)?; diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index c85b1ce2b9..98d13b4cd6 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -52,7 +52,6 @@ use crate::exec::SandboxType; use crate::exec::StdoutStream; use crate::exec::process_exec_tool_call; use crate::exec_env::create_env; -use crate::git_info::collect_git_info; use crate::mcp_connection_manager::McpConnectionManager; use crate::mcp_tool_call::handle_mcp_tool_call; use crate::models::ContentItem; @@ -1228,7 +1227,6 @@ async fn run_turn( base_instructions_override: sess.base_instructions.clone(), environment_context: Some(EnvironmentContext { cwd: sess.cwd.clone(), - git_info: collect_git_info(&sess.cwd).await, approval_policy: sess.approval_policy, sandbox_policy: sess.sandbox_policy.clone(), }), diff --git a/codex-rs/mcp-server/tests/send_message.rs b/codex-rs/mcp-server/tests/send_message.rs index 6e1389093c..f06c1587e3 100644 --- a/codex-rs/mcp-server/tests/send_message.rs +++ b/codex-rs/mcp-server/tests/send_message.rs @@ -99,7 +99,7 @@ async fn test_send_message_success() { response ); // wait for the server to hear the user message - sleep(Duration::from_secs(10)); + sleep(Duration::from_secs(5)); // Ensure the server and tempdir live until end of test drop(server); From ffe24991b7157ca27c78bb3ac4d422225bf7c031 Mon Sep 17 00:00:00 2001 From: Charlie Weems Date: Wed, 6 Aug 2025 09:10:23 -0700 Subject: [PATCH 0043/1309] Initial implementation of /init (#1822) Basic /init command that appends an instruction to create AGENTS.md to the conversation history. --- INIT.md | 40 +++++++++++++++++ codex-rs/tui/src/app.rs | 7 +++ codex-rs/tui/src/bottom_pane/chat_composer.rs | 44 +++++++++++++++++++ codex-rs/tui/src/bottom_pane/command_popup.rs | 35 +++++++++++++++ codex-rs/tui/src/chatwidget.rs | 10 +++++ codex-rs/tui/src/slash_command.rs | 2 + 6 files changed, 138 insertions(+) create mode 100644 INIT.md diff --git a/INIT.md b/INIT.md new file mode 100644 index 0000000000..b8fd3886b3 --- /dev/null +++ b/INIT.md @@ -0,0 +1,40 @@ +Generate a file named AGENTS.md that serves as a contributor guide for this repository. +Your goal is to produce a clear, concise, and well-structured document with descriptive headings and actionable explanations for each section. +Follow the outline below, but adapt as needed — add sections if relevant, and omit those that do not apply to this project. + +Document Requirements + +- Title the document "Repository Guidelines". +- Use Markdown headings (#, ##, etc.) for structure. +- Keep the document concise. 200-400 words is optimal. +- Keep explanations short, direct, and specific to this repository. +- Provide examples where helpful (commands, directory paths, naming patterns). +- Maintain a professional, instructional tone. + +Recommended Sections + +Project Structure & Module Organization + +- Outline the project structure, including where the source code, tests, and assets are located. + +Build, Test, and Development Commands + +- List key commands for building, testing, and running locally (e.g., npm test, make build). +- Briefly explain what each command does. + +Coding Style & Naming Conventions + +- Specify indentation rules, language-specific style preferences, and naming patterns. +- Include any formatting or linting tools used. + +Testing Guidelines + +- Identify testing frameworks and coverage requirements. +- State test naming conventions and how to run tests. + +Commit & Pull Request Guidelines + +- Summarize commit message conventions found in the project’s Git history. +- Outline pull request requirements (descriptions, linked issues, screenshots, etc.). + +(Optional) Add other sections if relevant, such as Security & Configuration Tips, Architecture Overview, or Agent-Specific Instructions. diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index eee2a61c2e..f1807da1c9 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -300,6 +300,13 @@ impl App<'_> { self.app_state = AppState::Chat { widget: new_widget }; self.app_event_tx.send(AppEvent::RequestRedraw); } + SlashCommand::Init => { + // Guard: do not run if a task is active. + if let AppState::Chat { widget } = &mut self.app_state { + const INIT_PROMPT: &str = include_str!("../../../INIT.md"); + widget.submit_text_message(INIT_PROMPT.to_string()); + } + } SlashCommand::Compact => { if let AppState::Chat { widget } = &mut self.app_state { widget.clear_token_usage(); diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index c9ad719771..f30b980da9 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -729,6 +729,7 @@ impl WidgetRef for &ChatComposer { #[cfg(test)] mod tests { + use crate::app_event::AppEvent; use crate::bottom_pane::AppEventSender; use crate::bottom_pane::ChatComposer; use crate::bottom_pane::InputResult; @@ -1004,6 +1005,49 @@ mod tests { } } + #[test] + fn slash_init_dispatches_command_and_does_not_submit_literal_text() { + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + use std::sync::mpsc::TryRecvError; + + let (tx, rx) = std::sync::mpsc::channel(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new(true, sender, false); + + // Type the slash command. + for ch in [ + '/', 'i', 'n', 'i', 't', // "/init" + ] { + let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE)); + } + + // Press Enter to dispatch the selected command. + let (result, _needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + // When a slash command is dispatched, the composer should not submit + // literal text and should clear its textarea. + match result { + InputResult::None => {} + InputResult::Submitted(text) => { + panic!("expected command dispatch, but composer submitted literal text: {text}") + } + } + assert!(composer.textarea.is_empty(), "composer should be cleared"); + + // Verify a DispatchCommand event for the "init" command was sent. + match rx.try_recv() { + Ok(AppEvent::DispatchCommand(cmd)) => { + assert_eq!(cmd.command(), "init"); + } + Ok(_other) => panic!("unexpected app event"), + Err(TryRecvError::Empty) => panic!("expected a DispatchCommand event for '/init'"), + Err(TryRecvError::Disconnected) => panic!("app event channel disconnected"), + } + } + #[test] fn test_multiple_pastes_submission() { use crossterm::event::KeyCode; diff --git a/codex-rs/tui/src/bottom_pane/command_popup.rs b/codex-rs/tui/src/bottom_pane/command_popup.rs index 364a8472dc..1027df1a67 100644 --- a/codex-rs/tui/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui/src/bottom_pane/command_popup.rs @@ -188,3 +188,38 @@ impl WidgetRef for CommandPopup { table.render(area, buf); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn filter_includes_init_when_typing_prefix() { + let mut popup = CommandPopup::new(); + // Simulate the composer line starting with '/in' so the popup filters + // matching commands by prefix. + popup.on_composer_text_change("/in".to_string()); + + // Access the filtered list via the selected command and ensure that + // one of the matches is the new "init" command. + let matches = popup.filtered_commands(); + assert!( + matches.iter().any(|cmd| cmd.command() == "init"), + "expected '/init' to appear among filtered commands" + ); + } + + #[test] + fn selecting_init_by_exact_match() { + let mut popup = CommandPopup::new(); + popup.on_composer_text_change("/init".to_string()); + + // When an exact match exists, the selected command should be that + // command by default. + let selected = popup.selected_command(); + match selected { + Some(cmd) => assert_eq!(cmd.command(), "init"), + None => panic!("expected a selected command for exact match"), + } + } +} diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 6d03be783b..64b65b3d11 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -575,6 +575,16 @@ impl ChatWidget<'_> { } } + /// Programmatically submit a user text message as if typed in the + /// composer. The text will be added to conversation history and sent to + /// the agent. + pub(crate) fn submit_text_message(&mut self, text: String) { + if text.is_empty() { + return; + } + self.submit_user_message(text.into()); + } + pub(crate) fn token_usage(&self) -> &TokenUsage { &self.token_usage } diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index 85dde7a113..daa663884b 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -13,6 +13,7 @@ pub enum SlashCommand { // DO NOT ALPHA-SORT! Enum order is presentation order in the popup, so // more frequently used commands should be listed first. New, + Init, Compact, Diff, Status, @@ -26,6 +27,7 @@ impl SlashCommand { pub fn description(self) -> &'static str { match self { SlashCommand::New => "Start a new chat", + SlashCommand::Init => "Create an AGENTS.md file with instructions for Codex.", SlashCommand::Compact => "Compact the chat history", SlashCommand::Quit => "Exit the application", SlashCommand::Diff => "Show git diff (including untracked files)", From ae88b69b09f876a3017196a9cd66f83dac79d9d7 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 6 Aug 2025 10:39:58 -0700 Subject: [PATCH 0044/1309] fix: add more instructions to ensure GitHub Action reviews only the necessary code (#1887) Empirically, we have seen the GitHub Action comment on code outside of the PR, so try to provide additional instructions in the prompt to avoid this. --- .github/actions/codex/src/process-label.ts | 33 +++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/.github/actions/codex/src/process-label.ts b/.github/actions/codex/src/process-label.ts index 4b4361e118..60207efaaa 100644 --- a/.github/actions/codex/src/process-label.ts +++ b/.github/actions/codex/src/process-label.ts @@ -91,7 +91,38 @@ async function processLabel( labelConfig: LabelConfig, ): Promise { const template = labelConfig.getPromptTemplate(); - const populatedTemplate = await renderPromptTemplate(template, ctx); + + // If this is a review label, prepend explicit PR-diff scoping guidance to + // reduce out-of-scope feedback. Do this before rendering so placeholders in + // the guidance (e.g., {CODEX_ACTION_GITHUB_EVENT_PATH}) are substituted. + const isReview = label.toLowerCase().includes("review"); + const reviewScopeGuidance = ` +PR Diff Scope +- Only review changes between the PR's merge-base and head; do not comment on commits or files outside this range. +- Derive the base/head SHAs from the event JSON at {CODEX_ACTION_GITHUB_EVENT_PATH}, then compute and use the PR diff for all analysis and comments. + +Commands to determine scope +- Resolve SHAs: + - BASE_SHA=$(jq -r '.pull_request.base.sha // .pull_request.base.ref' "{CODEX_ACTION_GITHUB_EVENT_PATH}") + - HEAD_SHA=$(jq -r '.pull_request.head.sha // .pull_request.head.ref' "{CODEX_ACTION_GITHUB_EVENT_PATH}") + - BASE_SHA=$(git rev-parse "$BASE_SHA") + - HEAD_SHA=$(git rev-parse "$HEAD_SHA") +- Prefer triple-dot (merge-base) semantics for PR diffs: + - Changed commits: git log --oneline "$BASE_SHA...$HEAD_SHA" + - Changed files: git diff --name-status "$BASE_SHA...$HEAD_SHA" + - Review hunks: git diff -U0 "$BASE_SHA...$HEAD_SHA" + +Review rules +- Anchor every comment to a file and hunk present in git diff "$BASE_SHA...$HEAD_SHA". +- If you mention context outside the diff, label it as "Follow-up (outside this PR scope)" and keep it brief (<=2 bullets). +- Do not critique commits or files not reachable in the PR range (merge-base(base, head) → head). +`.trim(); + + const effectiveTemplate = isReview + ? `${reviewScopeGuidance}\n\n${template}` + : template; + + const populatedTemplate = await renderPromptTemplate(effectiveTemplate, ctx); // Always run Codex and post the resulting message as a comment. let commentBody = await runCodex(populatedTemplate, ctx); From 64f2f2eca227c1a9f8087f5a0a696c1e3d4fb934 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 6 Aug 2025 11:48:03 -0700 Subject: [PATCH 0045/1309] fix: support $CODEX_HOME/AGENTS.md instead of $CODEX_HOME/instructions.md (#1891) The docs and code do not match. It turns out the docs are "right" in they are what we have been meaning to support, so this PR updates the code: https://github.com/openai/codex/blob/ae88b69b09f876a3017196a9cd66f83dac79d9d7/README.md#L298-L302 Support for `instructions.md` is a holdover from the TypeScript CLI, so we are just going to drop support for it altogether rather than maintain it in perpetuity. --- codex-rs/core/src/config.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 0b53df5ab7..f48cc9340b 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -70,7 +70,7 @@ pub struct Config { /// who have opted into Zero Data Retention (ZDR). pub disable_response_storage: bool, - /// User-provided instructions from instructions.md. + /// User-provided instructions from AGENTS.md. pub user_instructions: Option, /// Base instructions override. @@ -575,7 +575,7 @@ impl Config { None => return None, }; - p.push("instructions.md"); + p.push("AGENTS.md"); std::fs::read_to_string(&p).ok().and_then(|s| { let s = s.trim(); if s.is_empty() { From 4344537742f321aca82889625f55b2271f5e1e97 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 6 Aug 2025 11:58:57 -0700 Subject: [PATCH 0046/1309] chore: rename INIT.md to prompt_for_init_command.md and move closer to usage (#1886) Addressing my post-commit review feedback on https://github.com/openai/codex/pull/1822. --- INIT.md => codex-rs/tui/prompt_for_init_command.md | 0 codex-rs/tui/src/app.rs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename INIT.md => codex-rs/tui/prompt_for_init_command.md (100%) diff --git a/INIT.md b/codex-rs/tui/prompt_for_init_command.md similarity index 100% rename from INIT.md rename to codex-rs/tui/prompt_for_init_command.md diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index f1807da1c9..47e20287bd 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -303,7 +303,7 @@ impl App<'_> { SlashCommand::Init => { // Guard: do not run if a task is active. if let AppState::Chat { widget } = &mut self.app_state { - const INIT_PROMPT: &str = include_str!("../../../INIT.md"); + const INIT_PROMPT: &str = include_str!("../prompt_for_init_command.md"); widget.submit_text_message(INIT_PROMPT.to_string()); } } From 081caa5a6b77cde2624d414f280af6f0701fb22f Mon Sep 17 00:00:00 2001 From: Jeremy Rose <172423086+nornagon-openai@users.noreply.github.com> Date: Wed, 6 Aug 2025 12:03:45 -0700 Subject: [PATCH 0047/1309] show a transient history cell for commands (#1824) Adds a new "active history cell" for history bits that need to render more than once before they're inserted into the history. Only used for commands right now. https://github.com/user-attachments/assets/925f01a0-e56d-4613-bc25-fdaa85d8aea5 --------- Co-authored-by: easong-openai --- codex-rs/tui/src/chatwidget.rs | 47 ++++++++-- codex-rs/tui/src/history_cell.rs | 143 +++++++++++++++++-------------- 2 files changed, 119 insertions(+), 71 deletions(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 64b65b3d11..69f1600cd2 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -30,6 +30,8 @@ use codex_core::protocol::TurnDiffEvent; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; use ratatui::buffer::Buffer; +use ratatui::layout::Constraint; +use ratatui::layout::Layout; use ratatui::layout::Rect; use ratatui::widgets::Widget; use ratatui::widgets::WidgetRef; @@ -62,6 +64,7 @@ pub(crate) struct ChatWidget<'a> { app_event_tx: AppEventSender, codex_op_tx: UnboundedSender, bottom_pane: BottomPane<'a>, + active_history_cell: Option, config: Config, initial_user_message: Option, token_usage: TokenUsage, @@ -107,6 +110,17 @@ fn create_initial_user_message(text: String, image_paths: Vec) -> Optio } impl ChatWidget<'_> { + fn layout_areas(&self, area: Rect) -> [Rect; 2] { + Layout::vertical([ + Constraint::Max( + self.active_history_cell + .as_ref() + .map_or(0, |c| c.desired_height(area.width)), + ), + Constraint::Min(self.bottom_pane.desired_height(area.width)), + ]) + .areas(area) + } fn emit_stream_header(&mut self, kind: StreamKind) { use ratatui::text::Line as RLine; if self.stream_header_emitted { @@ -178,6 +192,7 @@ impl ChatWidget<'_> { has_input_focus: true, enhanced_keys_supported, }), + active_history_cell: None, config, initial_user_message: create_initial_user_message( initial_prompt.unwrap_or_default(), @@ -197,6 +212,10 @@ impl ChatWidget<'_> { pub fn desired_height(&self, width: u16) -> u16 { self.bottom_pane.desired_height(width) + + self + .active_history_cell + .as_ref() + .map_or(0, |c| c.desired_height(width)) } pub(crate) fn handle_key_event(&mut self, key_event: KeyEvent) { @@ -425,9 +444,11 @@ impl ChatWidget<'_> { cwd: cwd.clone(), }, ); - self.add_to_history(HistoryCell::new_active_exec_command(command)); + self.active_history_cell = Some(HistoryCell::new_active_exec_command(command)); + } + EventMsg::ExecCommandOutputDelta(_) => { + // TODO } - EventMsg::ExecCommandOutputDelta(_) => {} EventMsg::PatchApplyBegin(PatchApplyBeginEvent { call_id: _, auto_approved, @@ -438,8 +459,12 @@ impl ChatWidget<'_> { changes, )); } - EventMsg::PatchApplyEnd(patch_apply_end_event) => { - self.add_to_history(HistoryCell::new_patch_end_event(patch_apply_end_event)); + EventMsg::PatchApplyEnd(event) => { + self.add_to_history(HistoryCell::new_patch_apply_end( + event.stdout, + event.stderr, + event.success, + )); } EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id, @@ -450,6 +475,7 @@ impl ChatWidget<'_> { }) => { // Compute summary before moving stdout into the history cell. let cmd = self.running_commands.remove(&call_id); + self.active_history_cell = None; self.add_to_history(HistoryCell::new_completed_exec_command( cmd.map(|cmd| cmd.command).unwrap_or_else(|| vec![call_id]), CommandOutput { @@ -543,6 +569,7 @@ impl ChatWidget<'_> { CancellationEvent::Ignored => {} } if self.bottom_pane.is_task_running() { + self.active_history_cell = None; self.bottom_pane.clear_ctrl_c_quit_hint(); self.submit_op(Op::Interrupt); self.bottom_pane.set_task_running(false); @@ -596,7 +623,8 @@ impl ChatWidget<'_> { } pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> { - self.bottom_pane.cursor_pos(area) + let [_, bottom_pane_area] = self.layout_areas(area); + self.bottom_pane.cursor_pos(bottom_pane_area) } } @@ -700,10 +728,11 @@ impl ChatWidget<'_> { impl WidgetRef for &ChatWidget<'_> { fn render_ref(&self, area: Rect, buf: &mut Buffer) { - // In the hybrid inline viewport mode we only draw the interactive - // bottom pane; history entries are injected directly into scrollback - // via `Terminal::insert_before`. - (&self.bottom_pane).render(area, buf); + let [active_cell_area, bottom_pane_area] = self.layout_areas(area); + (&self.bottom_pane).render(bottom_pane_area, buf); + if let Some(cell) = &self.active_history_cell { + cell.render_ref(active_cell_area, buf); + } } } diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 5b7d9246f7..facb0e0a8f 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -11,7 +11,6 @@ use codex_core::plan_tool::StepStatus; use codex_core::plan_tool::UpdatePlanArgs; use codex_core::protocol::FileChange; use codex_core::protocol::McpInvocation; -use codex_core::protocol::PatchApplyEndEvent; use codex_core::protocol::SessionConfiguredEvent; use codex_core::protocol::TokenUsage; use image::DynamicImage; @@ -24,6 +23,9 @@ use ratatui::style::Modifier; use ratatui::style::Style; use ratatui::text::Line as RtLine; use ratatui::text::Span as RtSpan; +use ratatui::widgets::Paragraph; +use ratatui::widgets::WidgetRef; +use ratatui::widgets::Wrap; use std::collections::HashMap; use std::io::Cursor; use std::path::PathBuf; @@ -62,35 +64,23 @@ fn line_to_static(line: &Line) -> Line<'static> { /// scrollable list. pub(crate) enum HistoryCell { /// Welcome message. - WelcomeMessage { - view: TextBlock, - }, + WelcomeMessage { view: TextBlock }, /// Message from the user. - UserPrompt { - view: TextBlock, - }, + UserPrompt { view: TextBlock }, // AgentMessage and AgentReasoning variants were unused and have been removed. /// An exec tool call that has not finished yet. - ActiveExecCommand { - view: TextBlock, - }, + ActiveExecCommand { view: TextBlock }, /// Completed exec tool call. - CompletedExecCommand { - view: TextBlock, - }, + CompletedExecCommand { view: TextBlock }, /// An MCP tool call that has not finished yet. - ActiveMcpToolCall { - view: TextBlock, - }, + ActiveMcpToolCall { view: TextBlock }, /// Completed MCP tool call where we show the result serialized as JSON. - CompletedMcpToolCall { - view: TextBlock, - }, + CompletedMcpToolCall { view: TextBlock }, /// Completed MCP tool call where the result is an image. /// Admittedly, [mcp_types::CallToolResult] can have multiple content types, @@ -100,51 +90,34 @@ pub(crate) enum HistoryCell { // resized version avoids doing the potentially expensive rescale twice // because the scroll-view first calls `height()` for layouting and then // `render_window()` for painting. - CompletedMcpToolCallWithImageOutput { - _image: DynamicImage, - }, + CompletedMcpToolCallWithImageOutput { _image: DynamicImage }, /// Background event. - BackgroundEvent { - view: TextBlock, - }, + BackgroundEvent { view: TextBlock }, /// Output from the `/diff` command. - GitDiffOutput { - view: TextBlock, - }, + GitDiffOutput { view: TextBlock }, /// Output from the `/status` command. - StatusOutput { - view: TextBlock, - }, + StatusOutput { view: TextBlock }, /// Error event from the backend. - ErrorEvent { - view: TextBlock, - }, + ErrorEvent { view: TextBlock }, /// Info describing the newly-initialized session. - SessionInfo { - view: TextBlock, - }, + SessionInfo { view: TextBlock }, /// A pending code patch that is awaiting user approval. Mirrors the /// behaviour of `ActiveExecCommand` so the user sees *what* patch the /// model wants to apply before being prompted to approve or deny it. - PendingPatch { - view: TextBlock, - }, - - PatchEventEnd { - view: TextBlock, - }, + PendingPatch { view: TextBlock }, /// A human‑friendly rendering of the model's current plan and step /// statuses provided via the `update_plan` tool. - PlanUpdate { - view: TextBlock, - }, + PlanUpdate { view: TextBlock }, + + /// Result of applying a patch (success or failure) with optional output. + PatchApplyResult { view: TextBlock }, } const TOOL_CALL_MAX_LINES: usize = 5; @@ -165,8 +138,8 @@ impl HistoryCell { | HistoryCell::CompletedExecCommand { view } | HistoryCell::CompletedMcpToolCall { view } | HistoryCell::PendingPatch { view } - | HistoryCell::PatchEventEnd { view } | HistoryCell::PlanUpdate { view } + | HistoryCell::PatchApplyResult { view } | HistoryCell::ActiveExecCommand { view, .. } | HistoryCell::ActiveMcpToolCall { view, .. } => { view.lines.iter().map(line_to_static).collect() @@ -177,6 +150,15 @@ impl HistoryCell { ], } } + + pub(crate) fn desired_height(&self, width: u16) -> u16 { + Paragraph::new(Text::from(self.plain_lines())) + .wrap(Wrap { trim: false }) + .line_count(width) + .try_into() + .unwrap_or(0) + } + pub(crate) fn new_session_info( config: &Config, event: SessionConfiguredEvent, @@ -612,7 +594,10 @@ impl HistoryCell { PatchEventType::ApplyBegin { auto_approved: false, } => { - let lines = vec![Line::from("patch applied".magenta().bold())]; + let lines: Vec> = vec![ + Line::from("applying patch".magenta().bold()), + Line::from(""), + ]; return Self::PendingPatch { view: TextBlock::new(lines), }; @@ -661,29 +646,63 @@ impl HistoryCell { } } - pub(crate) fn new_patch_end_event(patch_apply_end_event: PatchApplyEndEvent) -> Self { - let PatchApplyEndEvent { - call_id: _, - stdout: _, - stderr, - success, - } = patch_apply_end_event; + pub(crate) fn new_patch_apply_end(stdout: String, stderr: String, success: bool) -> Self { + let mut lines: Vec> = Vec::new(); - let mut lines: Vec> = if success { - vec![Line::from("patch applied successfully".italic())] + let status = if success { + RtSpan::styled("patch applied", Style::default().fg(Color::Green)) } else { - let mut lines = vec![Line::from("patch failed".italic())]; - lines.extend(stderr.lines().map(|l| Line::from(l.to_string()))); - lines + RtSpan::styled( + "patch failed", + Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), + ) }; + lines.push(RtLine::from(vec![ + "patch".magenta().bold(), + " ".into(), + status, + ])); + + let src = if success { + if stdout.trim().is_empty() { + &stderr + } else { + &stdout + } + } else if stderr.trim().is_empty() { + &stdout + } else { + &stderr + }; + + if !src.trim().is_empty() { + lines.push(Line::from("")); + let mut iter = src.lines(); + for raw in iter.by_ref().take(TOOL_CALL_MAX_LINES) { + lines.push(ansi_escape_line(raw).dim()); + } + let remaining = iter.count(); + if remaining > 0 { + lines.push(Line::from(format!("... {remaining} additional lines")).dim()); + } + } + lines.push(Line::from("")); - HistoryCell::PatchEventEnd { + HistoryCell::PatchApplyResult { view: TextBlock::new(lines), } } } +impl WidgetRef for &HistoryCell { + fn render_ref(&self, area: Rect, buf: &mut Buffer) { + Paragraph::new(Text::from(self.plain_lines())) + .wrap(Wrap { trim: false }) + .render(area, buf); + } +} + fn create_diff_summary(changes: HashMap) -> Vec { // Build a concise, human‑readable summary list similar to the // `git status` short format so the user can reason about the From 8262ba58b28631377f2f63bcbe11b69997673d2c Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Wed, 6 Aug 2025 13:02:00 -0700 Subject: [PATCH 0048/1309] Prefer env var auth over default codex auth (#1861) ## Summary - Prioritize provider-specific API keys over default Codex auth when building requests - Add test to ensure provider env var auth overrides default auth ## Testing - `just fmt` - `just fix` *(fails: `let` expressions in this position are unstable)* - `cargo test --all-features` *(fails: `let` expressions in this position are unstable)* ------ https://chatgpt.com/codex/tasks/task_i_68926a104f7483208f2c8fd36763e0e3 --- codex-rs/core/src/client.rs | 6 +- codex-rs/core/src/config.rs | 2 +- codex-rs/core/src/model_provider_info.rs | 43 ++++++------ codex-rs/core/tests/client.rs | 82 +++++++++++++++++++++- codex-rs/core/tests/stream_no_completed.rs | 2 +- codex-rs/tui/src/lib.rs | 2 +- 6 files changed, 107 insertions(+), 30 deletions(-) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 9748cde7cb..ed05fb5db0 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -623,7 +623,7 @@ mod tests { request_max_retries: Some(0), stream_max_retries: Some(0), stream_idle_timeout_ms: Some(1000), - requires_auth: false, + requires_openai_auth: false, }; let events = collect_events( @@ -683,7 +683,7 @@ mod tests { request_max_retries: Some(0), stream_max_retries: Some(0), stream_idle_timeout_ms: Some(1000), - requires_auth: false, + requires_openai_auth: false, }; let events = collect_events(&[sse1.as_bytes()], provider).await; @@ -786,7 +786,7 @@ mod tests { request_max_retries: Some(0), stream_max_retries: Some(0), stream_idle_timeout_ms: Some(1000), - requires_auth: false, + requires_openai_auth: false, }; let out = run_sse(evs, provider).await; diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index f48cc9340b..63a2e5949f 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -842,7 +842,7 @@ disable_response_storage = true request_max_retries: Some(4), stream_max_retries: Some(10), stream_idle_timeout_ms: Some(300_000), - requires_auth: false, + requires_openai_auth: false, }; let model_provider_map = { let mut model_provider_map = built_in_model_providers(); diff --git a/codex-rs/core/src/model_provider_info.rs b/codex-rs/core/src/model_provider_info.rs index db369df3b7..a980211199 100644 --- a/codex-rs/core/src/model_provider_info.rs +++ b/codex-rs/core/src/model_provider_info.rs @@ -9,7 +9,6 @@ use codex_login::AuthMode; use codex_login::CodexAuth; use serde::Deserialize; use serde::Serialize; -use std::borrow::Cow; use std::collections::HashMap; use std::env::VarError; use std::time::Duration; @@ -79,7 +78,7 @@ pub struct ModelProviderInfo { /// Whether this provider requires some form of standard authentication (API key, ChatGPT token). #[serde(default)] - pub requires_auth: bool, + pub requires_openai_auth: bool, } impl ModelProviderInfo { @@ -87,26 +86,32 @@ impl ModelProviderInfo { /// reqwest Client applying: /// • provider-specific headers (static + env based) /// • Bearer auth header when an API key is available. + /// • Auth token for OAuth. /// - /// When `require_api_key` is true and the provider declares an `env_key` - /// but the variable is missing/empty, returns an [`Err`] identical to the + /// If the provider declares an `env_key` but the variable is missing/empty, returns an [`Err`] identical to the /// one produced by [`ModelProviderInfo::api_key`]. pub async fn create_request_builder<'a>( &'a self, client: &'a reqwest::Client, auth: &Option, ) -> crate::error::Result { - let auth: Cow<'_, Option> = if auth.is_some() { - Cow::Borrowed(auth) - } else { - Cow::Owned(self.get_fallback_auth()?) + let effective_auth = match self.api_key() { + Ok(Some(key)) => Some(CodexAuth::from_api_key(key)), + Ok(None) => auth.clone(), + Err(err) => { + if auth.is_some() { + auth.clone() + } else { + return Err(err); + } + } }; - let url = self.get_full_url(&auth); + let url = self.get_full_url(&effective_auth); let mut builder = client.post(url); - if let Some(auth) = auth.as_ref() { + if let Some(auth) = effective_auth.as_ref() { builder = builder.bearer_auth(auth.get_token().await?); } @@ -216,14 +221,6 @@ impl ModelProviderInfo { .map(Duration::from_millis) .unwrap_or(Duration::from_millis(DEFAULT_STREAM_IDLE_TIMEOUT_MS)) } - - fn get_fallback_auth(&self) -> crate::error::Result> { - let api_key = self.api_key()?; - if let Some(api_key) = api_key { - return Ok(Some(CodexAuth::from_api_key(api_key))); - } - Ok(None) - } } const DEFAULT_OLLAMA_PORT: u32 = 11434; @@ -275,7 +272,7 @@ pub fn built_in_model_providers() -> HashMap { request_max_retries: None, stream_max_retries: None, stream_idle_timeout_ms: None, - requires_auth: true, + requires_openai_auth: true, }, ), (BUILT_IN_OSS_MODEL_PROVIDER_ID, create_oss_provider()), @@ -319,7 +316,7 @@ pub fn create_oss_provider_with_base_url(base_url: &str) -> ModelProviderInfo { request_max_retries: None, stream_max_retries: None, stream_idle_timeout_ms: None, - requires_auth: false, + requires_openai_auth: false, } } @@ -347,7 +344,7 @@ base_url = "http://localhost:11434/v1" request_max_retries: None, stream_max_retries: None, stream_idle_timeout_ms: None, - requires_auth: false, + requires_openai_auth: false, }; let provider: ModelProviderInfo = toml::from_str(azure_provider_toml).unwrap(); @@ -376,7 +373,7 @@ query_params = { api-version = "2025-04-01-preview" } request_max_retries: None, stream_max_retries: None, stream_idle_timeout_ms: None, - requires_auth: false, + requires_openai_auth: false, }; let provider: ModelProviderInfo = toml::from_str(azure_provider_toml).unwrap(); @@ -408,7 +405,7 @@ env_http_headers = { "X-Example-Env-Header" = "EXAMPLE_ENV_VAR" } request_max_retries: None, stream_max_retries: None, stream_idle_timeout_ms: None, - requires_auth: false, + requires_openai_auth: false, }; let provider: ModelProviderInfo = toml::from_str(azure_provider_toml).unwrap(); diff --git a/codex-rs/core/tests/client.rs b/codex-rs/core/tests/client.rs index 00f91a879e..60eb922474 100644 --- a/codex-rs/core/tests/client.rs +++ b/codex-rs/core/tests/client.rs @@ -458,7 +458,7 @@ async fn azure_overrides_assign_properties_used_for_responses_url() { request_max_retries: None, stream_max_retries: None, stream_idle_timeout_ms: None, - requires_auth: false, + requires_openai_auth: false, }; // Init session @@ -481,6 +481,86 @@ async fn azure_overrides_assign_properties_used_for_responses_url() { wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn env_var_overrides_loaded_auth() { + #![allow(clippy::unwrap_used)] + + let existing_env_var_with_random_value = if cfg!(windows) { "USERNAME" } else { "USER" }; + + // Mock server + let server = MockServer::start().await; + + // First request – must NOT include `previous_response_id`. + let first = ResponseTemplate::new(200) + .insert_header("content-type", "text/event-stream") + .set_body_raw(sse_completed("resp1"), "text/event-stream"); + + // Expect POST to /openai/responses with api-version query param + Mock::given(method("POST")) + .and(path("/openai/responses")) + .and(query_param("api-version", "2025-04-01-preview")) + .and(header_regex("Custom-Header", "Value")) + .and(header_regex( + "Authorization", + format!( + "Bearer {}", + std::env::var(existing_env_var_with_random_value).unwrap() + ) + .as_str(), + )) + .respond_with(first) + .expect(1) + .mount(&server) + .await; + + let provider = ModelProviderInfo { + name: "custom".to_string(), + base_url: Some(format!("{}/openai", server.uri())), + // Reuse the existing environment variable to avoid using unsafe code + env_key: Some(existing_env_var_with_random_value.to_string()), + query_params: Some(std::collections::HashMap::from([( + "api-version".to_string(), + "2025-04-01-preview".to_string(), + )])), + env_key_instructions: None, + wire_api: WireApi::Responses, + http_headers: Some(std::collections::HashMap::from([( + "Custom-Header".to_string(), + "Value".to_string(), + )])), + env_http_headers: None, + request_max_retries: None, + stream_max_retries: None, + stream_idle_timeout_ms: None, + requires_openai_auth: false, + }; + + // Init session + let codex_home = TempDir::new().unwrap(); + let mut config = load_default_config_for_test(&codex_home); + config.model_provider = provider; + + let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new()); + let CodexSpawnOk { codex, .. } = Codex::spawn( + config, + Some(auth_from_token("Default Access Token".to_string())), + ctrl_c.clone(), + ) + .await + .unwrap(); + + codex + .submit(Op::UserInput { + items: vec![InputItem::Text { + text: "hello".into(), + }], + }) + .await + .unwrap(); + + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; +} + fn auth_from_token(id_token: String) -> CodexAuth { CodexAuth::new( None, diff --git a/codex-rs/core/tests/stream_no_completed.rs b/codex-rs/core/tests/stream_no_completed.rs index 3e30d93709..8a4216b129 100644 --- a/codex-rs/core/tests/stream_no_completed.rs +++ b/codex-rs/core/tests/stream_no_completed.rs @@ -90,7 +90,7 @@ async fn retries_on_early_close() { request_max_retries: Some(0), stream_max_retries: Some(1), stream_idle_timeout_ms: Some(2000), - requires_auth: false, + requires_openai_auth: false, }; let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new()); diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 50535e5967..0228a56859 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -287,7 +287,7 @@ fn restore() { #[allow(clippy::unwrap_used)] fn should_show_login_screen(config: &Config) -> bool { - if config.model_provider.requires_auth { + if config.model_provider.requires_openai_auth { // Reading the OpenAI API key is an async operation because it may need // to refresh the token. Block on it. let codex_home = config.codex_home.clone(); From 6cef86f05b0f3b8e3904666f728416ba0503babd Mon Sep 17 00:00:00 2001 From: ae Date: Wed, 6 Aug 2025 14:36:48 -0700 Subject: [PATCH 0049/1309] feat: update launch screen (#1881) - Updates the launch screen to: ``` >_ You are using OpenAI Codex in ~/code/codex/codex-rs Try one of the following commands to get started: 1. /init - Create an AGENTS.md file with instructions for Codex 2. /status - Show current session configuration and token usage 3. /compact - Compact the chat history 4. /new - Start a new chat ``` - These aren't the perfect commands, but as more land soon we can update. - We should also add logic later to make /init only show when there's no existing AGENTS.md. - Majorly need to iterate on copy. image --- codex-rs/tui/src/history_cell.rs | 41 +++++++++++++++++-------------- codex-rs/tui/src/slash_command.rs | 2 +- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index facb0e0a8f..c577ce17a0 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -1,4 +1,6 @@ +use crate::exec_command::relativize_to_home; use crate::exec_command::strip_bash_lc_and_escape; +use crate::slash_command::SlashCommand; use crate::text_block::TextBlock; use crate::text_formatting::format_and_truncate_tool_result; use base64::Engine; @@ -166,32 +168,35 @@ impl HistoryCell { ) -> Self { let SessionConfiguredEvent { model, - session_id, + session_id: _, history_log_id: _, history_entry_count: _, } = event; if is_first_event { - const VERSION: &str = env!("CARGO_PKG_VERSION"); + let cwd_str = match relativize_to_home(&config.cwd) { + Some(rel) if !rel.as_os_str().is_empty() => format!("~/{}", rel.display()), + Some(_) => "~".to_string(), + None => config.cwd.display().to_string(), + }; - let mut lines: Vec> = vec![ + let lines: Vec> = vec![ Line::from(vec![ - "OpenAI ".into(), - "Codex".bold(), - format!(" v{VERSION}").into(), - " (research preview)".dim(), - ]), - Line::from(""), - Line::from(vec![ - "codex session".magenta().bold(), - " ".into(), - session_id.to_string().dim(), + Span::raw(">_ ").dim(), + Span::styled( + "You are using OpenAI Codex in", + Style::default().add_modifier(Modifier::BOLD), + ), + Span::raw(format!(" {cwd_str}")).dim(), ]), + Line::from("".dim()), + Line::from(" Try one of the following commands to get started:".dim()), + Line::from("".dim()), + Line::from(format!(" 1. /init - {}", SlashCommand::Init.description()).dim()), + Line::from(format!(" 2. /status - {}", SlashCommand::Status.description()).dim()), + Line::from(format!(" 3. /compact - {}", SlashCommand::Compact.description()).dim()), + Line::from(format!(" 4. /new - {}", SlashCommand::New.description()).dim()), + Line::from("".dim()), ]; - - for (key, value) in create_config_summary_entries(config) { - lines.push(Line::from(vec![format!("{key}: ").bold(), value.into()])); - } - lines.push(Line::from("")); HistoryCell::WelcomeMessage { view: TextBlock::new(lines), } diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index daa663884b..75bca641ac 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -27,7 +27,7 @@ impl SlashCommand { pub fn description(self) -> &'static str { match self { SlashCommand::New => "Start a new chat", - SlashCommand::Init => "Create an AGENTS.md file with instructions for Codex.", + SlashCommand::Init => "Create an AGENTS.md file with instructions for Codex", SlashCommand::Compact => "Compact the chat history", SlashCommand::Quit => "Exit the application", SlashCommand::Diff => "Show git diff (including untracked files)", From a575effbb0bb687c6c2f88574484f577ded7a38e Mon Sep 17 00:00:00 2001 From: ae Date: Wed, 6 Aug 2025 14:56:34 -0700 Subject: [PATCH 0050/1309] feat: interrupt running task on ctrl-z (#1880) - Arguably a bugfix as previously CTRL-Z didn't do anything. - Only in TUI mode for now. This may make sense in other modes... to be researched. - The TUI runs the terminal in raw mode and the signals arrive as key events, so we handle CTRL-Z as a key event just like CTRL-C. - Not adding UI for it as a composer redesign is coming, and we can just add it then. - We should follow with CTRL-Z a second time doing the native terminal action. --- codex-rs/tui/src/app.rs | 10 ++++++++++ codex-rs/tui/src/chatwidget.rs | 33 +++++++++++++++++++++------------ 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 47e20287bd..2ac550b00e 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -238,6 +238,16 @@ impl App<'_> { } } } + KeyEvent { + code: KeyCode::Char('z'), + modifiers: crossterm::event::KeyModifiers::CONTROL, + kind: KeyEventKind::Press, + .. + } => { + if let AppState::Chat { widget } = &mut self.app_state { + widget.on_ctrl_z(); + } + } KeyEvent { code: KeyCode::Char('d'), modifiers: crossterm::event::KeyModifiers::CONTROL, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 69f1600cd2..128e1ae8c1 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -110,6 +110,22 @@ fn create_initial_user_message(text: String, image_paths: Vec) -> Optio } impl ChatWidget<'_> { + fn interrupt_running_task(&mut self) { + if self.bottom_pane.is_task_running() { + self.active_history_cell = None; + self.bottom_pane.clear_ctrl_c_quit_hint(); + self.submit_op(Op::Interrupt); + self.bottom_pane.set_task_running(false); + self.bottom_pane.clear_live_ring(); + self.live_builder = RowBuilder::new(self.live_builder.width()); + self.current_stream = None; + self.stream_header_emitted = false; + self.answer_buffer.clear(); + self.reasoning_buffer.clear(); + self.content_buffer.clear(); + self.request_redraw(); + } + } fn layout_areas(&self, area: Rect) -> [Rect; 2] { Layout::vertical([ Constraint::Max( @@ -569,18 +585,7 @@ impl ChatWidget<'_> { CancellationEvent::Ignored => {} } if self.bottom_pane.is_task_running() { - self.active_history_cell = None; - self.bottom_pane.clear_ctrl_c_quit_hint(); - self.submit_op(Op::Interrupt); - self.bottom_pane.set_task_running(false); - self.bottom_pane.clear_live_ring(); - self.live_builder = RowBuilder::new(self.live_builder.width()); - self.current_stream = None; - self.stream_header_emitted = false; - self.answer_buffer.clear(); - self.reasoning_buffer.clear(); - self.content_buffer.clear(); - self.request_redraw(); + self.interrupt_running_task(); CancellationEvent::Ignored } else if self.bottom_pane.ctrl_c_quit_hint_visible() { self.submit_op(Op::Shutdown); @@ -591,6 +596,10 @@ impl ChatWidget<'_> { } } + pub(crate) fn on_ctrl_z(&mut self) { + self.interrupt_running_task(); + } + pub(crate) fn composer_is_empty(&self) -> bool { self.bottom_pane.composer_is_empty() } From f25b2e8e2c6ae4a83ec0ea76e3f0a7924fff0533 Mon Sep 17 00:00:00 2001 From: Dylan Date: Wed, 6 Aug 2025 14:58:53 -0700 Subject: [PATCH 0051/1309] Propagate apply_patch filesystem errors (#1892) ## Summary We have been returning `exit code 0` from the apply patch command when writes fail, which causes our `exec` harness to pass back confusing messages to the model. Instead, we should loudly fail so that the harness and the model can handle these errors appropriately. Also adds a test to confirm this behavior. ## Testing - `cargo test -p codex-apply-patch` --- codex-rs/apply-patch/src/lib.rs | 43 ++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/codex-rs/apply-patch/src/lib.rs b/codex-rs/apply-patch/src/lib.rs index 8d42be9a92..61b1b68f9e 100644 --- a/codex-rs/apply-patch/src/lib.rs +++ b/codex-rs/apply-patch/src/lib.rs @@ -42,6 +42,15 @@ impl From for ApplyPatchError { } } +impl From<&std::io::Error> for ApplyPatchError { + fn from(err: &std::io::Error) -> Self { + ApplyPatchError::IoError(IoError { + context: "I/O error".to_string(), + source: std::io::Error::new(err.kind(), err.to_string()), + }) + } +} + #[derive(Debug, Error)] #[error("{context}: {source}")] pub struct IoError { @@ -366,13 +375,21 @@ pub fn apply_hunks( match apply_hunks_to_files(hunks) { Ok(affected) => { print_summary(&affected, stdout).map_err(ApplyPatchError::from)?; + Ok(()) } Err(err) => { - writeln!(stderr, "{err:?}").map_err(ApplyPatchError::from)?; + let msg = err.to_string(); + writeln!(stderr, "{msg}").map_err(ApplyPatchError::from)?; + if let Some(io) = err.downcast_ref::() { + Err(ApplyPatchError::from(io)) + } else { + Err(ApplyPatchError::IoError(IoError { + context: msg, + source: std::io::Error::other(err), + })) + } } } - - Ok(()) } /// Applies each parsed patch hunk to the filesystem. @@ -1238,4 +1255,24 @@ g }) ); } + + #[test] + fn test_apply_patch_fails_on_write_error() { + let dir = tempdir().unwrap(); + let path = dir.path().join("readonly.txt"); + fs::write(&path, "before\n").unwrap(); + let mut perms = fs::metadata(&path).unwrap().permissions(); + perms.set_readonly(true); + fs::set_permissions(&path, perms).unwrap(); + + let patch = wrap_patch(&format!( + "*** Update File: {}\n@@\n-before\n+after\n*** End Patch", + path.display() + )); + + let mut stdout = Vec::new(); + let mut stderr = Vec::new(); + let result = apply_patch(&patch, &mut stdout, &mut stderr); + assert!(result.is_err()); + } } From 2d5de795aaf38310e7753166fc04f2dff991a7e2 Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Wed, 6 Aug 2025 15:22:14 -0700 Subject: [PATCH 0052/1309] First pass at a TUI onboarding (#1876) This sets up the scaffolding and basic flow for a TUI onboarding experience. It covers sign in with ChatGPT, env auth, as well as some safety guidance. Next up: 1. Replace the git warning screen 2. Use this to configure default approval/sandbox modes Note the shimmer flashes are from me slicing the video, not jank. https://github.com/user-attachments/assets/0fbe3479-fdde-41f3-87fb-a7a83ab895b8 --- codex-rs/cli/src/main.rs | 4 +- codex-rs/core/src/protocol.rs | 6 + codex-rs/login/src/lib.rs | 57 +++- codex-rs/login/src/login_with_chatgpt.py | 2 +- codex-rs/tui/src/app.rs | 64 +++- codex-rs/tui/src/app_event.rs | 3 + codex-rs/tui/src/colors.rs | 4 + codex-rs/tui/src/lib.rs | 22 +- codex-rs/tui/src/main.rs | 4 +- codex-rs/tui/src/onboarding/auth.rs | 316 ++++++++++++++++++ codex-rs/tui/src/onboarding/mod.rs | 3 + .../tui/src/onboarding/onboarding_screen.rs | 157 +++++++++ codex-rs/tui/src/onboarding/welcome.rs | 23 ++ codex-rs/tui/src/shimmer.rs | 84 +++++ 14 files changed, 724 insertions(+), 25 deletions(-) create mode 100644 codex-rs/tui/src/colors.rs create mode 100644 codex-rs/tui/src/onboarding/auth.rs create mode 100644 codex-rs/tui/src/onboarding/mod.rs create mode 100644 codex-rs/tui/src/onboarding/onboarding_screen.rs create mode 100644 codex-rs/tui/src/onboarding/welcome.rs create mode 100644 codex-rs/tui/src/shimmer.rs diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 27f8312193..c43365c7d5 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -121,7 +121,9 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() let mut tui_cli = cli.interactive; prepend_config_flags(&mut tui_cli.config_overrides, cli.config_overrides); let usage = codex_tui::run_main(tui_cli, codex_linux_sandbox_exe).await?; - println!("{}", codex_core::protocol::FinalOutput::from(usage)); + if !usage.is_zero() { + println!("{}", codex_core::protocol::FinalOutput::from(usage)); + } } Some(Subcommand::Exec(mut exec_cli)) => { prepend_config_flags(&mut exec_cli.config_overrides, cli.config_overrides); diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs index 55000fb6d7..052806dd97 100644 --- a/codex-rs/core/src/protocol.rs +++ b/codex-rs/core/src/protocol.rs @@ -429,6 +429,12 @@ pub struct TokenUsage { pub total_tokens: u64, } +impl TokenUsage { + pub fn is_zero(&self) -> bool { + self.total_tokens == 0 + } +} + #[derive(Debug, Clone, Deserialize, Serialize)] pub struct FinalOutput { pub token_usage: TokenUsage, diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index 35f67e7109..95bc119ec5 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -4,6 +4,7 @@ use chrono::Utc; use serde::Deserialize; use serde::Serialize; use std::env; +use std::fs::File; use std::fs::OpenOptions; use std::io::Read; use std::io::Write; @@ -11,6 +12,7 @@ use std::io::Write; use std::os::unix::fs::OpenOptionsExt; use std::path::Path; use std::path::PathBuf; +use std::process::Child; use std::process::Stdio; use std::sync::Arc; use std::sync::Mutex; @@ -183,6 +185,59 @@ fn get_auth_file(codex_home: &Path) -> PathBuf { codex_home.join("auth.json") } +/// Represents a running login subprocess. The child can be killed by holding +/// the mutex and calling `kill()`. +#[derive(Debug, Clone)] +pub struct SpawnedLogin { + pub child: Arc>, + pub stdout: Arc>>, + pub stderr: Arc>>, +} + +/// Spawn the ChatGPT login Python server as a child process and return a handle to its process. +pub fn spawn_login_with_chatgpt(codex_home: &Path) -> std::io::Result { + let mut cmd = std::process::Command::new("python3"); + cmd.arg("-c") + .arg(SOURCE_FOR_PYTHON_SERVER) + .env("CODEX_HOME", codex_home) + .env("CODEX_CLIENT_ID", CLIENT_ID) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + let mut child = cmd.spawn()?; + + let stdout_buf = Arc::new(Mutex::new(Vec::new())); + let stderr_buf = Arc::new(Mutex::new(Vec::new())); + + if let Some(mut out) = child.stdout.take() { + let buf = stdout_buf.clone(); + std::thread::spawn(move || { + let mut tmp = Vec::new(); + let _ = std::io::copy(&mut out, &mut tmp); + if let Ok(mut b) = buf.lock() { + b.extend_from_slice(&tmp); + } + }); + } + if let Some(mut err) = child.stderr.take() { + let buf = stderr_buf.clone(); + std::thread::spawn(move || { + let mut tmp = Vec::new(); + let _ = std::io::copy(&mut err, &mut tmp); + if let Ok(mut b) = buf.lock() { + b.extend_from_slice(&tmp); + } + }); + } + + Ok(SpawnedLogin { + child: Arc::new(Mutex::new(child)), + stdout: stdout_buf, + stderr: stderr_buf, + }) +} + /// Run `python3 -c {{SOURCE_FOR_PYTHON_SERVER}}` with the CODEX_HOME /// environment variable set to the provided `codex_home` path. If the /// subprocess exits 0, read the OPENAI_API_KEY property out of @@ -234,7 +289,7 @@ pub fn login_with_api_key(codex_home: &Path, api_key: &str) -> std::io::Result<( /// Attempt to read and refresh the `auth.json` file in the given `CODEX_HOME` directory. /// Returns the full AuthDotJson structure after refreshing if necessary. pub fn try_read_auth_json(auth_file: &Path) -> std::io::Result { - let mut file = std::fs::File::open(auth_file)?; + let mut file = File::open(auth_file)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; let auth_dot_json: AuthDotJson = serde_json::from_str(&contents)?; diff --git a/codex-rs/login/src/login_with_chatgpt.py b/codex-rs/login/src/login_with_chatgpt.py index 14ccfa9ed4..317c95769c 100644 --- a/codex-rs/login/src/login_with_chatgpt.py +++ b/codex-rs/login/src/login_with_chatgpt.py @@ -110,7 +110,7 @@ def main() -> None: eprint(f"Failed to open browser: {e}") eprint( - f"If your browser did not open, navigate to this URL to authenticate:\n\n{auth_url}" + f". If your browser did not open, navigate to this URL to authenticate: \n\n{auth_url}" ) # Run the server in the main thread until `shutdown()` is called by the diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 2ac550b00e..23e12be38f 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -5,6 +5,10 @@ use crate::file_search::FileSearchManager; use crate::get_git_diff::get_git_diff; use crate::git_warning_screen::GitWarningOutcome; use crate::git_warning_screen::GitWarningScreen; +use crate::onboarding::onboarding_screen::KeyEventResult; +use crate::onboarding::onboarding_screen::KeyboardHandler; +use crate::onboarding::onboarding_screen::OnboardingScreen; +use crate::should_show_login_screen; use crate::slash_command::SlashCommand; use crate::tui; use codex_core::config::Config; @@ -35,6 +39,9 @@ const REDRAW_DEBOUNCE: Duration = Duration::from_millis(10); /// Top-level application state: which full-screen view is currently active. #[allow(clippy::large_enum_variant)] enum AppState<'a> { + Onboarding { + screen: OnboardingScreen, + }, /// The main chat UI is visible. Chat { /// Boxed to avoid a large enum variant and reduce the overall size of @@ -42,7 +49,9 @@ enum AppState<'a> { widget: Box>, }, /// The start-up warning that recommends running codex inside a Git repo. - GitWarning { screen: GitWarningScreen }, + GitWarning { + screen: GitWarningScreen, + }, } pub(crate) struct App<'a> { @@ -133,7 +142,20 @@ impl App<'_> { }); } - let (app_state, chat_args) = if show_git_warning { + let show_login_screen = should_show_login_screen(&config); + let (app_state, chat_args) = if show_login_screen { + ( + AppState::Onboarding { + screen: OnboardingScreen::new(app_event_tx.clone(), config.codex_home.clone()), + }, + Some(ChatWidgetArgs { + config: config.clone(), + initial_prompt, + initial_images, + enhanced_keys_supported, + }), + ) + } else if show_git_warning { ( AppState::GitWarning { screen: GitWarningScreen::new(), @@ -232,6 +254,9 @@ impl App<'_> { AppState::Chat { widget } => { widget.on_ctrl_c(); } + AppState::Onboarding { .. } => { + self.app_event_tx.send(AppEvent::ExitRequest); + } AppState::GitWarning { .. } => { // Allow exiting the app with Ctrl+C from the warning screen. self.app_event_tx.send(AppEvent::ExitRequest); @@ -265,6 +290,9 @@ impl App<'_> { self.dispatch_key_event(key_event); } } + AppState::Onboarding { .. } => { + self.app_event_tx.send(AppEvent::ExitRequest); + } AppState::GitWarning { .. } => { self.app_event_tx.send(AppEvent::ExitRequest); } @@ -292,10 +320,12 @@ impl App<'_> { } AppEvent::CodexOp(op) => match &mut self.app_state { AppState::Chat { widget } => widget.submit_op(op), + AppState::Onboarding { .. } => {} AppState::GitWarning { .. } => {} }, AppEvent::LatestLog(line) => match &mut self.app_state { AppState::Chat { widget } => widget.update_latest_log(line), + AppState::Onboarding { .. } => {} AppState::GitWarning { .. } => {} }, AppEvent::DispatchCommand(command) => match command { @@ -392,6 +422,12 @@ impl App<'_> { })); } }, + AppEvent::OnboardingAuthComplete(result) => { + if let AppState::Onboarding { screen } = &mut self.app_state { + // Let the onboarding screen handle success/failure and emit follow-up events. + let _ = screen.on_auth_complete(result); + } + } AppEvent::StartFileSearch(query) => { self.file_search.on_user_query(query); } @@ -410,6 +446,7 @@ impl App<'_> { pub(crate) fn token_usage(&self) -> codex_core::protocol::TokenUsage { match &self.app_state { AppState::Chat { widget } => widget.token_usage().clone(), + AppState::Onboarding { .. } => codex_core::protocol::TokenUsage::default(), AppState::GitWarning { .. } => codex_core::protocol::TokenUsage::default(), } } @@ -438,6 +475,7 @@ impl App<'_> { let size = terminal.size()?; let desired_height = match &self.app_state { AppState::Chat { widget } => widget.desired_height(size.width), + AppState::Onboarding { .. } => size.height, AppState::GitWarning { .. } => size.height, }; @@ -468,6 +506,7 @@ impl App<'_> { } frame.render_widget_ref(&**widget, frame.area()) } + AppState::Onboarding { screen } => frame.render_widget_ref(&*screen, frame.area()), AppState::GitWarning { screen } => frame.render_widget_ref(&*screen, frame.area()), })?; Ok(()) @@ -480,6 +519,25 @@ impl App<'_> { AppState::Chat { widget } => { widget.handle_key_event(key_event); } + AppState::Onboarding { screen } => match screen.handle_key_event(key_event) { + KeyEventResult::Continue => { + self.app_state = AppState::Chat { + widget: Box::new(ChatWidget::new( + self.config.clone(), + self.app_event_tx.clone(), + None, + Vec::new(), + self.enhanced_keys_supported, + )), + }; + } + KeyEventResult::Quit => { + self.app_event_tx.send(AppEvent::ExitRequest); + } + KeyEventResult::None => { + // do nothing + } + }, AppState::GitWarning { screen } => match screen.handle_key_event(key_event) { GitWarningOutcome::Continue => { // User accepted – switch to chat view. @@ -511,6 +569,7 @@ impl App<'_> { fn dispatch_paste_event(&mut self, pasted: String) { match &mut self.app_state { AppState::Chat { widget } => widget.handle_paste(pasted), + AppState::Onboarding { .. } => {} AppState::GitWarning { .. } => {} } } @@ -518,6 +577,7 @@ impl App<'_> { fn dispatch_codex_event(&mut self, event: Event) { match &mut self.app_state { AppState::Chat { widget } => widget.handle_codex_event(event), + AppState::Onboarding { .. } => {} AppState::GitWarning { .. } => {} } } diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 77a600d304..7df7761ac5 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -48,4 +48,7 @@ pub(crate) enum AppEvent { }, InsertHistory(Vec>), + + /// Onboarding: result of login_with_chatgpt. + OnboardingAuthComplete(Result<(), String>), } diff --git a/codex-rs/tui/src/colors.rs b/codex-rs/tui/src/colors.rs new file mode 100644 index 0000000000..0ba386df37 --- /dev/null +++ b/codex-rs/tui/src/colors.rs @@ -0,0 +1,4 @@ +use ratatui::style::Color; + +pub(crate) const LIGHT_BLUE: Color = Color::Rgb(134, 238, 255); +pub(crate) const SUCCESS_GREEN: Color = Color::Rgb(169, 230, 158); diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 0228a56859..e65083bfe1 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -13,7 +13,6 @@ use codex_login::load_auth; use codex_ollama::DEFAULT_OSS_MODEL; use log_layer::TuiLogLayer; use std::fs::OpenOptions; -use std::io::Write; use std::path::PathBuf; use tracing::error; use tracing_appender::non_blocking; @@ -27,6 +26,7 @@ mod bottom_pane; mod chatwidget; mod citation_regex; mod cli; +mod colors; pub mod custom_terminal; mod exec_command; mod file_search; @@ -37,6 +37,8 @@ pub mod insert_history; pub mod live_wrap; mod log_layer; mod markdown; +pub mod onboarding; +mod shimmer; mod slash_command; mod status_indicator_widget; mod text_block; @@ -204,24 +206,6 @@ pub async fn run_main( eprintln!(""); } - let show_login_screen = should_show_login_screen(&config); - if show_login_screen { - std::io::stdout() - .write_all(b"No API key detected.\nLogin with your ChatGPT account? [Yn] ")?; - std::io::stdout().flush()?; - let mut input = String::new(); - std::io::stdin().read_line(&mut input)?; - let trimmed = input.trim(); - if !(trimmed.is_empty() || trimmed.eq_ignore_ascii_case("y")) { - std::process::exit(1); - } - // Spawn a task to run the login command. - // Block until the login command is finished. - codex_login::login_with_chatgpt(&config.codex_home, false).await?; - - std::io::stdout().write_all(b"Login successful.\n")?; - } - // Determine whether we need to display the "not a git repo" warning // modal. The flag is shown when the current working directory is *not* // inside a Git repository **and** the user did *not* pass the diff --git a/codex-rs/tui/src/main.rs b/codex-rs/tui/src/main.rs index 209febf035..2dbd797dd4 100644 --- a/codex-rs/tui/src/main.rs +++ b/codex-rs/tui/src/main.rs @@ -22,7 +22,9 @@ fn main() -> anyhow::Result<()> { .raw_overrides .splice(0..0, top_cli.config_overrides.raw_overrides); let usage = run_main(inner, codex_linux_sandbox_exe).await?; - println!("{}", codex_core::protocol::FinalOutput::from(usage)); + if !usage.is_zero() { + println!("{}", codex_core::protocol::FinalOutput::from(usage)); + } Ok(()) }) } diff --git a/codex-rs/tui/src/onboarding/auth.rs b/codex-rs/tui/src/onboarding/auth.rs new file mode 100644 index 0000000000..834c36819e --- /dev/null +++ b/codex-rs/tui/src/onboarding/auth.rs @@ -0,0 +1,316 @@ +use crossterm::event::KeyCode; +use crossterm::event::KeyEvent; +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::prelude::Widget; +use ratatui::style::Color; +use ratatui::style::Modifier; +use ratatui::style::Style; +use ratatui::text::Line; +use ratatui::text::Span; +use ratatui::widgets::Paragraph; +use ratatui::widgets::WidgetRef; +use ratatui::widgets::Wrap; + +use codex_login::AuthMode; + +use crate::app_event::AppEvent; +use crate::app_event_sender::AppEventSender; +use crate::colors::LIGHT_BLUE; +use crate::colors::SUCCESS_GREEN; +use crate::onboarding::onboarding_screen::KeyEventResult; +use crate::onboarding::onboarding_screen::KeyboardHandler; +use crate::shimmer::FrameTicker; +use crate::shimmer::shimmer_spans; +use std::path::PathBuf; +// no additional imports + +#[derive(Debug)] +pub(crate) enum SignInState { + PickMode, + ChatGptContinueInBrowser(#[allow(dead_code)] ContinueInBrowserState), + ChatGptSuccess, +} + +#[derive(Debug)] +/// Used to manage the lifecycle of SpawnedLogin and FrameTicker and ensure they get cleaned up. +pub(crate) struct ContinueInBrowserState { + _login_child: Option, + _frame_ticker: Option, +} + +impl Drop for ContinueInBrowserState { + fn drop(&mut self) { + if let Some(child) = &self._login_child { + if let Ok(mut locked) = child.child.lock() { + // Best-effort terminate and reap the child to avoid zombies. + let _ = locked.kill(); + let _ = locked.wait(); + } + } + } +} + +impl KeyboardHandler for AuthModeWidget { + fn handle_key_event(&mut self, key_event: KeyEvent) -> KeyEventResult { + match key_event.code { + KeyCode::Up | KeyCode::Char('k') => { + self.mode = AuthMode::ChatGPT; + KeyEventResult::None + } + KeyCode::Down | KeyCode::Char('j') => { + self.mode = AuthMode::ApiKey; + KeyEventResult::None + } + KeyCode::Char('1') => { + self.mode = AuthMode::ChatGPT; + self.start_chatgpt_login(); + KeyEventResult::None + } + KeyCode::Char('2') => { + self.mode = AuthMode::ApiKey; + self.verify_api_key() + } + KeyCode::Enter => match self.mode { + AuthMode::ChatGPT => match &self.sign_in_state { + SignInState::PickMode => self.start_chatgpt_login(), + SignInState::ChatGptContinueInBrowser(_) => KeyEventResult::None, + SignInState::ChatGptSuccess => KeyEventResult::Continue, + }, + AuthMode::ApiKey => self.verify_api_key(), + }, + KeyCode::Esc => { + if matches!(self.sign_in_state, SignInState::ChatGptContinueInBrowser(_)) { + self.sign_in_state = SignInState::PickMode; + self.event_tx.send(AppEvent::RequestRedraw); + KeyEventResult::None + } else { + KeyEventResult::Quit + } + } + KeyCode::Char('q') => KeyEventResult::Quit, + _ => KeyEventResult::None, + } + } +} + +#[derive(Debug)] +pub(crate) struct AuthModeWidget { + pub mode: AuthMode, + pub error: Option, + pub sign_in_state: SignInState, + pub event_tx: AppEventSender, + pub codex_home: PathBuf, +} + +impl AuthModeWidget { + fn render_pick_mode(&self, area: Rect, buf: &mut Buffer) { + let mut lines: Vec = vec![ + Line::from(vec![ + Span::raw("> "), + Span::styled( + "Sign in with your ChatGPT account?", + Style::default().add_modifier(Modifier::BOLD), + ), + ]), + Line::from(""), + ]; + + let create_mode_item = |idx: usize, + selected_mode: AuthMode, + text: &str, + description: &str| + -> Vec> { + let is_selected = self.mode == selected_mode; + let caret = if is_selected { ">" } else { " " }; + + let line1 = if is_selected { + Line::from(vec![ + Span::styled( + format!("{} {}. ", caret, idx + 1), + Style::default().fg(LIGHT_BLUE).add_modifier(Modifier::DIM), + ), + Span::styled(text.to_owned(), Style::default().fg(LIGHT_BLUE)), + ]) + } else { + Line::from(format!(" {}. {text}", idx + 1)) + }; + + let line2 = if is_selected { + Line::from(format!(" {description}")) + .style(Style::default().fg(LIGHT_BLUE).add_modifier(Modifier::DIM)) + } else { + Line::from(format!(" {description}")) + .style(Style::default().add_modifier(Modifier::DIM)) + }; + + vec![line1, line2] + }; + + lines.extend(create_mode_item( + 0, + AuthMode::ChatGPT, + "Sign in with ChatGPT or create a new account", + "Leverages your plan, starting at $20 a month for Plus", + )); + lines.extend(create_mode_item( + 1, + AuthMode::ApiKey, + "Provide your own API key", + "Pay only for what you use", + )); + lines.push(Line::from("")); + lines.push( + Line::from("Press Enter to continue") + .style(Style::default().add_modifier(Modifier::DIM)), + ); + if let Some(err) = &self.error { + lines.push(Line::from("")); + lines.push(Line::from(Span::styled( + err.as_str(), + Style::default().fg(Color::Red), + ))); + } + + Paragraph::new(lines) + .wrap(Wrap { trim: false }) + .render(area, buf); + } + + fn render_continue_in_browser(&self, area: Rect, buf: &mut Buffer) { + let idx = self.current_frame(); + let mut spans = vec![Span::from("> ")]; + spans.extend(shimmer_spans("Finish signing in via your browser", idx)); + let lines = vec![ + Line::from(spans), + Line::from(""), + Line::from(" Press Escape to cancel") + .style(Style::default().add_modifier(Modifier::DIM)), + ]; + Paragraph::new(lines) + .wrap(Wrap { trim: false }) + .render(area, buf); + } + + fn render_chatgpt_success(&self, area: Rect, buf: &mut Buffer) { + let lines = vec![ + Line::from("✓ Signed in with your ChatGPT account") + .style(Style::default().fg(SUCCESS_GREEN)), + Line::from(""), + Line::from("> Before you start:"), + Line::from(""), + Line::from(" Codex can make mistakes"), + Line::from(" Check important info") + .style(Style::default().add_modifier(Modifier::DIM)), + Line::from(""), + Line::from(" Due to prompt injection risks, only use it with code you trust"), + Line::from(" For more details see https://github.com/openai/codex") + .style(Style::default().add_modifier(Modifier::DIM)), + Line::from(""), + Line::from(" Powered by your ChatGPT account"), + Line::from(" Uses your plan's rate limits and training data preferences") + .style(Style::default().add_modifier(Modifier::DIM)), + Line::from(""), + Line::from(" Press Enter to continue").style(Style::default().fg(LIGHT_BLUE)), + ]; + + Paragraph::new(lines) + .wrap(Wrap { trim: false }) + .render(area, buf); + } + + fn start_chatgpt_login(&mut self) -> KeyEventResult { + self.error = None; + match codex_login::spawn_login_with_chatgpt(&self.codex_home) { + Ok(child) => { + self.spawn_completion_poller(child.clone()); + self.sign_in_state = + SignInState::ChatGptContinueInBrowser(ContinueInBrowserState { + _login_child: Some(child), + _frame_ticker: Some(FrameTicker::new(self.event_tx.clone())), + }); + self.event_tx.send(AppEvent::RequestRedraw); + KeyEventResult::None + } + Err(e) => { + self.sign_in_state = SignInState::PickMode; + self.error = Some(e.to_string()); + self.event_tx.send(AppEvent::RequestRedraw); + KeyEventResult::None + } + } + } + + /// TODO: Read/write from the correct hierarchy config overrides + auth json + OPENAI_API_KEY. + fn verify_api_key(&mut self) -> KeyEventResult { + if std::env::var("OPENAI_API_KEY").is_err() { + self.error = + Some("Set OPENAI_API_KEY in your environment. Learn more: https://platform.openai.com/docs/libraries".to_string()); + self.event_tx.send(AppEvent::RequestRedraw); + KeyEventResult::None + } else { + KeyEventResult::Continue + } + } + + fn spawn_completion_poller(&self, child: codex_login::SpawnedLogin) { + let child_arc = child.child.clone(); + let stderr_buf = child.stderr.clone(); + let event_tx = self.event_tx.clone(); + std::thread::spawn(move || { + loop { + let done = { + if let Ok(mut locked) = child_arc.lock() { + match locked.try_wait() { + Ok(Some(status)) => Some(status.success()), + Ok(None) => None, + Err(_) => Some(false), + } + } else { + Some(false) + } + }; + if let Some(success) = done { + if success { + event_tx.send(AppEvent::OnboardingAuthComplete(Ok(()))); + } else { + let err = stderr_buf + .lock() + .ok() + .and_then(|b| String::from_utf8(b.clone()).ok()) + .unwrap_or_else(|| "login_with_chatgpt subprocess failed".to_string()); + event_tx.send(AppEvent::OnboardingAuthComplete(Err(err))); + } + break; + } + std::thread::sleep(std::time::Duration::from_millis(250)); + } + }); + } + + fn current_frame(&self) -> usize { + // Derive frame index from wall-clock time to avoid storing animation state. + // 100ms per frame to match the previous ticker cadence. + let now_ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0); + (now_ms / 100) as usize + } +} + +impl WidgetRef for AuthModeWidget { + fn render_ref(&self, area: Rect, buf: &mut Buffer) { + match self.sign_in_state { + SignInState::PickMode => { + self.render_pick_mode(area, buf); + } + SignInState::ChatGptContinueInBrowser(_) => { + self.render_continue_in_browser(area, buf); + } + SignInState::ChatGptSuccess => { + self.render_chatgpt_success(area, buf); + } + } + } +} diff --git a/codex-rs/tui/src/onboarding/mod.rs b/codex-rs/tui/src/onboarding/mod.rs new file mode 100644 index 0000000000..42d3ac8187 --- /dev/null +++ b/codex-rs/tui/src/onboarding/mod.rs @@ -0,0 +1,3 @@ +mod auth; +pub mod onboarding_screen; +mod welcome; diff --git a/codex-rs/tui/src/onboarding/onboarding_screen.rs b/codex-rs/tui/src/onboarding/onboarding_screen.rs new file mode 100644 index 0000000000..e2548bacb3 --- /dev/null +++ b/codex-rs/tui/src/onboarding/onboarding_screen.rs @@ -0,0 +1,157 @@ +use crossterm::event::KeyEvent; +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::widgets::WidgetRef; + +use codex_login::AuthMode; + +use crate::app_event::AppEvent; +use crate::app_event_sender::AppEventSender; +use crate::onboarding::auth::AuthModeWidget; +use crate::onboarding::auth::SignInState; +use crate::onboarding::welcome::WelcomeWidget; +use std::path::PathBuf; + +enum Step { + Welcome(WelcomeWidget), + Auth(AuthModeWidget), +} + +pub(crate) trait KeyboardHandler { + fn handle_key_event(&mut self, key_event: KeyEvent) -> KeyEventResult; +} + +pub(crate) enum KeyEventResult { + Continue, + Quit, + None, +} + +pub(crate) struct OnboardingScreen { + event_tx: AppEventSender, + steps: Vec, +} + +impl OnboardingScreen { + pub(crate) fn new(event_tx: AppEventSender, codex_home: PathBuf) -> Self { + let steps: Vec = vec![ + Step::Welcome(WelcomeWidget {}), + Step::Auth(AuthModeWidget { + event_tx: event_tx.clone(), + mode: AuthMode::ChatGPT, + error: None, + sign_in_state: SignInState::PickMode, + codex_home, + }), + ]; + Self { event_tx, steps } + } + + pub(crate) fn on_auth_complete(&mut self, result: Result<(), String>) -> KeyEventResult { + if let Some(Step::Auth(state)) = self.steps.last_mut() { + match result { + Ok(()) => { + state.sign_in_state = SignInState::ChatGptSuccess; + self.event_tx.send(AppEvent::RequestRedraw); + KeyEventResult::None + } + Err(e) => { + state.sign_in_state = SignInState::PickMode; + state.error = Some(e); + self.event_tx.send(AppEvent::RequestRedraw); + KeyEventResult::None + } + } + } else { + KeyEventResult::None + } + } +} + +impl KeyboardHandler for OnboardingScreen { + fn handle_key_event(&mut self, key_event: KeyEvent) -> KeyEventResult { + if let Some(last_step) = self.steps.last_mut() { + self.event_tx.send(AppEvent::RequestRedraw); + last_step.handle_key_event(key_event) + } else { + KeyEventResult::None + } + } +} + +impl WidgetRef for &OnboardingScreen { + fn render_ref(&self, area: Rect, buf: &mut Buffer) { + // Render steps top-to-bottom, measuring each step's height dynamically. + let mut y = area.y; + let bottom = area.y.saturating_add(area.height); + let width = area.width; + + // Helper to scan a temporary buffer and return number of used rows. + fn used_rows(tmp: &Buffer, width: u16, height: u16) -> u16 { + if width == 0 || height == 0 { + return 0; + } + let mut last_non_empty: Option = None; + for yy in 0..height { + let mut any = false; + for xx in 0..width { + let sym = tmp[(xx, yy)].symbol(); + if !sym.trim().is_empty() { + any = true; + break; + } + } + if any { + last_non_empty = Some(yy); + } + } + last_non_empty.map(|v| v + 2).unwrap_or(0) + } + + let mut i = 0usize; + while i < self.steps.len() && y < bottom { + let step = &self.steps[i]; + let max_h = bottom.saturating_sub(y); + if max_h == 0 || width == 0 { + break; + } + let scratch_area = Rect::new(0, 0, width, max_h); + let mut scratch = Buffer::empty(scratch_area); + step.render_ref(scratch_area, &mut scratch); + let h = used_rows(&scratch, width, max_h).min(max_h); + if h > 0 { + let target = Rect { + x: area.x, + y, + width, + height: h, + }; + step.render_ref(target, buf); + y = y.saturating_add(h); + } + i += 1; + } + } +} + +impl KeyboardHandler for Step { + fn handle_key_event(&mut self, key_event: KeyEvent) -> KeyEventResult { + match self { + Step::Welcome(_) => KeyEventResult::None, + Step::Auth(widget) => widget.handle_key_event(key_event), + } + } +} + +impl WidgetRef for Step { + fn render_ref(&self, area: Rect, buf: &mut Buffer) { + match self { + Step::Welcome(widget) => { + widget.render_ref(area, buf); + } + Step::Auth(widget) => { + widget.render_ref(area, buf); + } + } + } +} diff --git a/codex-rs/tui/src/onboarding/welcome.rs b/codex-rs/tui/src/onboarding/welcome.rs new file mode 100644 index 0000000000..e00e3004b6 --- /dev/null +++ b/codex-rs/tui/src/onboarding/welcome.rs @@ -0,0 +1,23 @@ +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::prelude::Widget; +use ratatui::style::Modifier; +use ratatui::style::Style; +use ratatui::text::Line; +use ratatui::text::Span; +use ratatui::widgets::WidgetRef; + +pub(crate) struct WelcomeWidget {} + +impl WidgetRef for &WelcomeWidget { + fn render_ref(&self, area: Rect, buf: &mut Buffer) { + let line = Line::from(vec![ + Span::raw("> "), + Span::styled( + "Welcome to Codex, OpenAI's coding agent that runs in your terminal", + Style::default().add_modifier(Modifier::BOLD), + ), + ]); + line.render(area, buf); + } +} diff --git a/codex-rs/tui/src/shimmer.rs b/codex-rs/tui/src/shimmer.rs new file mode 100644 index 0000000000..9d28b732a0 --- /dev/null +++ b/codex-rs/tui/src/shimmer.rs @@ -0,0 +1,84 @@ +use std::sync::Arc; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::time::Duration; + +use ratatui::style::Color; +use ratatui::style::Modifier; +use ratatui::style::Style; +use ratatui::text::Span; + +use crate::app_event::AppEvent; +use crate::app_event_sender::AppEventSender; + +#[derive(Debug)] +pub(crate) struct FrameTicker { + running: Arc, +} + +impl FrameTicker { + pub(crate) fn new(app_event_tx: AppEventSender) -> Self { + let running = Arc::new(AtomicBool::new(true)); + let running_clone = running.clone(); + let app_event_tx_clone = app_event_tx.clone(); + std::thread::spawn(move || { + while running_clone.load(Ordering::Relaxed) { + std::thread::sleep(Duration::from_millis(100)); + app_event_tx_clone.send(AppEvent::RequestRedraw); + } + }); + Self { running } + } +} + +impl Drop for FrameTicker { + fn drop(&mut self) { + self.running.store(false, Ordering::Relaxed); + } +} + +pub(crate) fn shimmer_spans(text: &str, frame_idx: usize) -> Vec> { + let chars: Vec = text.chars().collect(); + let padding = 10usize; + let period = chars.len() + padding * 2; + let pos = frame_idx % period; + let has_true_color = supports_color::on_cached(supports_color::Stream::Stdout) + .map(|level| level.has_16m) + .unwrap_or(false); + let band_half_width = 6.0; + + let mut spans: Vec> = Vec::with_capacity(chars.len()); + for (i, ch) in chars.iter().enumerate() { + let i_pos = i as isize + padding as isize; + let pos = pos as isize; + let dist = (i_pos - pos).abs() as f32; + + let t = if dist <= band_half_width { + let x = std::f32::consts::PI * (dist / band_half_width); + 0.5 * (1.0 + x.cos()) + } else { + 0.0 + }; + let brightness = 0.4 + 0.6 * t; + let level = (brightness * 255.0).clamp(0.0, 255.0) as u8; + let style = if has_true_color { + Style::default() + .fg(Color::Rgb(level, level, level)) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(color_for_level(level)) + }; + spans.push(Span::styled(ch.to_string(), style)); + } + spans +} + +fn color_for_level(level: u8) -> Color { + if level < 128 { + Color::DarkGray + } else if level < 192 { + Color::Gray + } else { + Color::White + } +} From 57c973b571b17e8709bfdee38c0848fae793d257 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Wed, 6 Aug 2025 16:14:02 -0700 Subject: [PATCH 0053/1309] Add 2025-08-06 model family (#1899) --- codex-rs/core/src/client.rs | 9 +++++++++ codex-rs/core/src/model_family.rs | 5 +++++ codex-rs/core/src/openai_model_info.rs | 5 +++++ 3 files changed, 19 insertions(+) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index ed05fb5db0..0fa143fdb7 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -127,6 +127,15 @@ impl ModelClient { let auth_mode = auth.as_ref().map(|a| a.mode); + if self.config.model_family.family == "2025-08-06-model" + && auth_mode != Some(AuthMode::ChatGPT) + { + return Err(CodexErr::UnexpectedStatus( + StatusCode::BAD_REQUEST, + "2025-08-06-model is only supported with ChatGPT auth, run `codex login status` to check your auth status and `codex login` to login with ChatGPT".to_string(), + )); + } + let store = prompt.store && auth_mode != Some(AuthMode::ChatGPT); let full_instructions = prompt.get_full_instructions(&self.config.model_family); diff --git a/codex-rs/core/src/model_family.rs b/codex-rs/core/src/model_family.rs index 7c4a9de6c2..cadbceca1e 100644 --- a/codex-rs/core/src/model_family.rs +++ b/codex-rs/core/src/model_family.rs @@ -89,6 +89,11 @@ pub fn find_family_for_model(slug: &str) -> Option { simple_model_family!(slug, "gpt-oss") } else if slug.starts_with("gpt-3.5") { simple_model_family!(slug, "gpt-3.5") + } else if slug.starts_with("2025-08-06-model") { + model_family!( + slug, "2025-08-06-model", + supports_reasoning_summaries: true, + ) } else { None } diff --git a/codex-rs/core/src/openai_model_info.rs b/codex-rs/core/src/openai_model_info.rs index 935eb8be4f..0ce94267d3 100644 --- a/codex-rs/core/src/openai_model_info.rs +++ b/codex-rs/core/src/openai_model_info.rs @@ -77,6 +77,11 @@ pub(crate) fn get_model_info(model_family: &ModelFamily) -> Option { max_output_tokens: 4_096, }), + "2025-08-06-model" => Some(ModelInfo { + context_window: 200_000, + max_output_tokens: 100_000, + }), + _ => None, } } From af8c1cdf12ca0418ee253abb43a0849da15fd571 Mon Sep 17 00:00:00 2001 From: pap-openai Date: Thu, 7 Aug 2025 00:16:47 +0100 Subject: [PATCH 0054/1309] fix meta+b meta+f (option+left/right) (#1895) Option+Left or Option+Right should move cursor to beginning/end of the word. We weren't listening to what terminals are sending (on MacOS) and were therefore printing b or f instead of moving cursor. We were actually in the first match clause and returning char insertion (https://github.com/openai/codex/pull/1895/files#diff-6bf130cd00438cc27a38c5a4d9937a27cf9a324c191de4b74fc96019d362be6dL209) Tested on Apple Terminal, iTerm, Ghostty --- codex-rs/tui/src/bottom_pane/textarea.rs | 53 +++++++++++++++++------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/textarea.rs b/codex-rs/tui/src/bottom_pane/textarea.rs index cb30c2ac7a..8e6e8b07a3 100644 --- a/codex-rs/tui/src/bottom_pane/textarea.rs +++ b/codex-rs/tui/src/bottom_pane/textarea.rs @@ -206,7 +206,10 @@ impl TextArea { match event { KeyEvent { code: KeyCode::Char(c), - modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT | KeyModifiers::ALT, + // Insert plain characters (and Shift-modified). Do NOT insert when ALT is held, + // because many terminals map Option/Meta combos to ALT+ (e.g. ESC f/ESC b) + // for word navigation. Those are handled explicitly below. + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, .. } => self.insert_str(&c.to_string()), KeyEvent { @@ -245,6 +248,23 @@ impl TextArea { } => { self.delete_backward_word(); } + // Meta-b -> move to beginning of previous word + // Meta-f -> move to end of next word + // Many terminals map Option (macOS) to Alt. Some send Alt|Shift, so match contains(ALT). + KeyEvent { + code: KeyCode::Char('b'), + modifiers: KeyModifiers::ALT, + .. + } => { + self.set_cursor(self.beginning_of_previous_word()); + } + KeyEvent { + code: KeyCode::Char('f'), + modifiers: KeyModifiers::ALT, + .. + } => { + self.set_cursor(self.end_of_next_word()); + } KeyEvent { code: KeyCode::Char('u'), modifiers: KeyModifiers::CONTROL, @@ -275,6 +295,23 @@ impl TextArea { } => { self.move_cursor_right(); } + // Some terminals send Alt+Arrow for word-wise movement: + // Option/Left -> Alt+Left (previous word start) + // Option/Right -> Alt+Right (next word end) + KeyEvent { + code: KeyCode::Left, + modifiers: KeyModifiers::ALT, + .. + } => { + self.set_cursor(self.beginning_of_previous_word()); + } + KeyEvent { + code: KeyCode::Right, + modifiers: KeyModifiers::ALT, + .. + } => { + self.set_cursor(self.end_of_next_word()); + } KeyEvent { code: KeyCode::Up, .. } => { @@ -312,20 +349,6 @@ impl TextArea { } => { self.move_cursor_to_end_of_line(true); } - KeyEvent { - code: KeyCode::Left, - modifiers: KeyModifiers::CONTROL | KeyModifiers::ALT, - .. - } => { - self.set_cursor(self.beginning_of_previous_word()); - } - KeyEvent { - code: KeyCode::Right, - modifiers: KeyModifiers::CONTROL | KeyModifiers::ALT, - .. - } => { - self.set_cursor(self.end_of_next_word()); - } o => { tracing::debug!("Unhandled key event in TextArea: {:?}", o); } From 8a980399c5f3941768b8e91e79e0b449d8eec8de Mon Sep 17 00:00:00 2001 From: pap-openai Date: Thu, 7 Aug 2025 00:58:06 +0100 Subject: [PATCH 0055/1309] fix cursor file name insert (#1896) Cursor wasn't moving when inserting a file, resulting in being not at the end of the filename when inserting the file. This fixes it by moving the cursor to the end of the file + one trailing space. Example screenshot after selecting a file when typing `@` image --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index f30b980da9..5d877253a6 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -453,6 +453,8 @@ impl ChatComposer { new_text.push_str(&text[end_idx..]); self.textarea.set_text(&new_text); + let new_cursor = start_idx.saturating_add(path.len()).saturating_add(1); + self.textarea.set_cursor(new_cursor); } /// Handle key event when no popup is visible. From a5e17cda6bde9106a79d6f49810e772395f6b065 Mon Sep 17 00:00:00 2001 From: aibrahim-oai Date: Wed, 6 Aug 2025 17:10:59 -0700 Subject: [PATCH 0056/1309] Run command UI (#1897) Edit how commands show: image --- codex-rs/tui/src/chatwidget.rs | 3 +- codex-rs/tui/src/history_cell.rs | 48 +++++++++++++++++--------------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 128e1ae8c1..50fa776ec3 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -485,7 +485,7 @@ impl ChatWidget<'_> { EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id, exit_code, - duration, + duration: _, stdout, stderr, }) => { @@ -498,7 +498,6 @@ impl ChatWidget<'_> { exit_code, stdout, stderr, - duration, }, )); } diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index c577ce17a0..5caedf98ab 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -38,7 +38,6 @@ pub(crate) struct CommandOutput { pub(crate) exit_code: i32, pub(crate) stdout: String, pub(crate) stderr: String, - pub(crate) duration: Duration, } pub(crate) enum PatchEventType { @@ -122,7 +121,7 @@ pub(crate) enum HistoryCell { PatchApplyResult { view: TextBlock }, } -const TOOL_CALL_MAX_LINES: usize = 5; +const TOOL_CALL_MAX_LINES: usize = 3; impl HistoryCell { /// Return a cloned, plain representation of the cell's lines suitable for @@ -232,8 +231,11 @@ impl HistoryCell { let command_escaped = strip_bash_lc_and_escape(&command); let lines: Vec> = vec![ - Line::from(vec!["command".magenta(), " running...".dim()]), - Line::from(format!("$ {command_escaped}")), + Line::from(vec![ + "▌ ".cyan(), + "Running command ".magenta(), + command_escaped.into(), + ]), Line::from(""), ]; @@ -247,34 +249,36 @@ impl HistoryCell { exit_code, stdout, stderr, - duration, } = output; let mut lines: Vec> = Vec::new(); - - // Title depends on whether we have output yet. - let title_line = Line::from(vec![ - "command".magenta(), - format!( - " (code: {}, duration: {})", - exit_code, - format_duration(duration) - ) - .dim(), - ]); - lines.push(title_line); + let command_escaped = strip_bash_lc_and_escape(&command); + lines.push(Line::from(vec![ + "⚡Ran command ".magenta(), + command_escaped.into(), + ])); let src = if exit_code == 0 { stdout } else { stderr }; - let cmdline = strip_bash_lc_and_escape(&command); - lines.push(Line::from(format!("$ {cmdline}"))); let mut lines_iter = src.lines(); - for raw in lines_iter.by_ref().take(TOOL_CALL_MAX_LINES) { - lines.push(ansi_escape_line(raw).dim()); + for (idx, raw) in lines_iter.by_ref().take(TOOL_CALL_MAX_LINES).enumerate() { + let mut line = ansi_escape_line(raw); + let prefix = if idx == 0 { " ⎿ " } else { " " }; + line.spans.insert(0, prefix.into()); + line.spans.iter_mut().for_each(|span| { + span.style = span.style.add_modifier(Modifier::DIM); + }); + lines.push(line); } let remaining = lines_iter.count(); if remaining > 0 { - lines.push(Line::from(format!("... {remaining} additional lines")).dim()); + let mut more = Line::from(format!("... +{remaining} lines")); + // Continuation/ellipsis is treated as a subsequent line for prefixing + more.spans.insert(0, " ".into()); + more.spans.iter_mut().for_each(|span| { + span.style = span.style.add_modifier(Modifier::DIM); + }); + lines.push(more); } lines.push(Line::from("")); From 8a990b5401ea9bb6557d136d46420cd1f130b4dc Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Wed, 6 Aug 2025 19:39:07 -0700 Subject: [PATCH 0057/1309] Migrate GitWarning to OnboardingScreen (#1915) This paves the way to do per-directory approval settings (https://github.com/openai/codex/pull/1912). This also lets us pass in a Config/ChatWidgetArgs into onboarding which can then mutate it and emit the ChatWidgetArgs it wants at the end which may be modified by the said approval settings. CleanShot 2025-08-06 at 19 30 55 --- codex-rs/core/src/util.rs | 7 +- codex-rs/exec/src/lib.rs | 2 +- codex-rs/tui/src/app.rs | 164 ++++++------------ codex-rs/tui/src/app_event.rs | 2 + codex-rs/tui/src/app_event_sender.rs | 2 +- codex-rs/tui/src/git_warning_screen.rs | 122 ------------- codex-rs/tui/src/lib.rs | 14 +- codex-rs/tui/src/onboarding/auth.rs | 121 +++++++++---- .../tui/src/onboarding/continue_to_chat.rs | 30 ++++ codex-rs/tui/src/onboarding/git_warning.rs | 126 ++++++++++++++ codex-rs/tui/src/onboarding/mod.rs | 2 + .../tui/src/onboarding/onboarding_screen.rs | 152 ++++++++++++---- codex-rs/tui/src/onboarding/welcome.rs | 17 +- 13 files changed, 443 insertions(+), 318 deletions(-) delete mode 100644 codex-rs/tui/src/git_warning_screen.rs create mode 100644 codex-rs/tui/src/onboarding/continue_to_chat.rs create mode 100644 codex-rs/tui/src/onboarding/git_warning.rs diff --git a/codex-rs/core/src/util.rs b/codex-rs/core/src/util.rs index a7c1485273..5ba1e25666 100644 --- a/codex-rs/core/src/util.rs +++ b/codex-rs/core/src/util.rs @@ -1,3 +1,4 @@ +use std::path::Path; use std::sync::Arc; use std::time::Duration; @@ -5,8 +6,6 @@ use rand::Rng; use tokio::sync::Notify; use tracing::debug; -use crate::config::Config; - const INITIAL_DELAY_MS: u64 = 200; const BACKOFF_FACTOR: f64 = 1.3; @@ -47,8 +46,8 @@ pub(crate) fn backoff(attempt: u64) -> Duration { /// `git worktree add` where the checkout lives outside the main repository /// directory. If you need Codex to work from such a checkout simply pass the /// `--allow-no-git-exec` CLI flag that disables the repo requirement. -pub fn is_inside_git_repo(config: &Config) -> bool { - let mut dir = config.cwd.to_path_buf(); +pub fn is_inside_git_repo(base_dir: &Path) -> bool { + let mut dir = base_dir.to_path_buf(); loop { if dir.join(".git").exists() { diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 06df2aebca..5d7f1281ee 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -180,7 +180,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any // is using. event_processor.print_config_summary(&config, &prompt); - if !skip_git_repo_check && !is_inside_git_repo(&config) { + if !skip_git_repo_check && !is_inside_git_repo(&config.cwd.to_path_buf()) { eprintln!("Not inside a Git repo and --skip-git-repo-check was not specified."); std::process::exit(1); } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 23e12be38f..ad3b4f3372 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -3,11 +3,9 @@ use crate::app_event_sender::AppEventSender; use crate::chatwidget::ChatWidget; use crate::file_search::FileSearchManager; use crate::get_git_diff::get_git_diff; -use crate::git_warning_screen::GitWarningOutcome; -use crate::git_warning_screen::GitWarningScreen; -use crate::onboarding::onboarding_screen::KeyEventResult; use crate::onboarding::onboarding_screen::KeyboardHandler; use crate::onboarding::onboarding_screen::OnboardingScreen; +use crate::onboarding::onboarding_screen::OnboardingScreenArgs; use crate::should_show_login_screen; use crate::slash_command::SlashCommand; use crate::tui; @@ -15,6 +13,7 @@ use codex_core::config::Config; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; use codex_core::protocol::Op; +use codex_core::util::is_inside_git_repo; use color_eyre::eyre::Result; use crossterm::SynchronizedUpdate; use crossterm::event::KeyCode; @@ -48,10 +47,6 @@ enum AppState<'a> { /// `AppState`. widget: Box>, }, - /// The start-up warning that recommends running codex inside a Git repo. - GitWarning { - screen: GitWarningScreen, - }, } pub(crate) struct App<'a> { @@ -69,17 +64,13 @@ pub(crate) struct App<'a> { pending_history_lines: Vec>, - /// Stored parameters needed to instantiate the ChatWidget later, e.g., - /// after dismissing the Git-repo warning. - chat_args: Option, - enhanced_keys_supported: bool, } /// Aggregate parameters needed to create a `ChatWidget`, as creation may be /// deferred until after the Git warning screen is dismissed. -#[derive(Clone)] -struct ChatWidgetArgs { +#[derive(Clone, Debug)] +pub(crate) struct ChatWidgetArgs { config: Config, initial_prompt: Option, initial_images: Vec, @@ -90,7 +81,7 @@ impl App<'_> { pub(crate) fn new( config: Config, initial_prompt: Option, - show_git_warning: bool, + skip_git_repo_check: bool, initial_images: Vec, ) -> Self { let (app_event_tx, app_event_rx) = channel(); @@ -143,30 +134,25 @@ impl App<'_> { } let show_login_screen = should_show_login_screen(&config); - let (app_state, chat_args) = if show_login_screen { - ( - AppState::Onboarding { - screen: OnboardingScreen::new(app_event_tx.clone(), config.codex_home.clone()), - }, - Some(ChatWidgetArgs { - config: config.clone(), - initial_prompt, - initial_images, - enhanced_keys_supported, + let show_git_warning = + !skip_git_repo_check && !is_inside_git_repo(&config.cwd.to_path_buf()); + let app_state = if show_login_screen || show_git_warning { + let chat_widget_args = ChatWidgetArgs { + config: config.clone(), + initial_prompt, + initial_images, + enhanced_keys_supported, + }; + AppState::Onboarding { + screen: OnboardingScreen::new(OnboardingScreenArgs { + event_tx: app_event_tx.clone(), + codex_home: config.codex_home.clone(), + cwd: config.cwd.clone(), + show_login_screen, + show_git_warning, + chat_widget_args, }), - ) - } else if show_git_warning { - ( - AppState::GitWarning { - screen: GitWarningScreen::new(), - }, - Some(ChatWidgetArgs { - config: config.clone(), - initial_prompt, - initial_images, - enhanced_keys_supported, - }), - ) + } } else { let chat_widget = ChatWidget::new( config.clone(), @@ -175,12 +161,9 @@ impl App<'_> { initial_images, enhanced_keys_supported, ); - ( - AppState::Chat { - widget: Box::new(chat_widget), - }, - None, - ) + AppState::Chat { + widget: Box::new(chat_widget), + } }; let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone()); @@ -192,7 +175,6 @@ impl App<'_> { config, file_search, pending_redraw, - chat_args, enhanced_keys_supported, } } @@ -249,20 +231,14 @@ impl App<'_> { modifiers: crossterm::event::KeyModifiers::CONTROL, kind: KeyEventKind::Press, .. - } => { - match &mut self.app_state { - AppState::Chat { widget } => { - widget.on_ctrl_c(); - } - AppState::Onboarding { .. } => { - self.app_event_tx.send(AppEvent::ExitRequest); - } - AppState::GitWarning { .. } => { - // Allow exiting the app with Ctrl+C from the warning screen. - self.app_event_tx.send(AppEvent::ExitRequest); - } + } => match &mut self.app_state { + AppState::Chat { widget } => { + widget.on_ctrl_c(); } - } + AppState::Onboarding { .. } => { + self.app_event_tx.send(AppEvent::ExitRequest); + } + }, KeyEvent { code: KeyCode::Char('z'), modifiers: crossterm::event::KeyModifiers::CONTROL, @@ -293,9 +269,6 @@ impl App<'_> { AppState::Onboarding { .. } => { self.app_event_tx.send(AppEvent::ExitRequest); } - AppState::GitWarning { .. } => { - self.app_event_tx.send(AppEvent::ExitRequest); - } } } KeyEvent { @@ -321,15 +294,14 @@ impl App<'_> { AppEvent::CodexOp(op) => match &mut self.app_state { AppState::Chat { widget } => widget.submit_op(op), AppState::Onboarding { .. } => {} - AppState::GitWarning { .. } => {} }, AppEvent::LatestLog(line) => match &mut self.app_state { AppState::Chat { widget } => widget.update_latest_log(line), AppState::Onboarding { .. } => {} - AppState::GitWarning { .. } => {} }, AppEvent::DispatchCommand(command) => match command { SlashCommand::New => { + // User accepted – switch to chat view. let new_widget = Box::new(ChatWidget::new( self.config.clone(), self.app_event_tx.clone(), @@ -424,8 +396,23 @@ impl App<'_> { }, AppEvent::OnboardingAuthComplete(result) => { if let AppState::Onboarding { screen } = &mut self.app_state { - // Let the onboarding screen handle success/failure and emit follow-up events. - let _ = screen.on_auth_complete(result); + screen.on_auth_complete(result); + } + } + AppEvent::OnboardingComplete(ChatWidgetArgs { + config, + enhanced_keys_supported, + initial_images, + initial_prompt, + }) => { + self.app_state = AppState::Chat { + widget: Box::new(ChatWidget::new( + config, + app_event_tx.clone(), + initial_prompt, + initial_images, + enhanced_keys_supported, + )), } } AppEvent::StartFileSearch(query) => { @@ -447,7 +434,6 @@ impl App<'_> { match &self.app_state { AppState::Chat { widget } => widget.token_usage().clone(), AppState::Onboarding { .. } => codex_core::protocol::TokenUsage::default(), - AppState::GitWarning { .. } => codex_core::protocol::TokenUsage::default(), } } @@ -476,7 +462,6 @@ impl App<'_> { let desired_height = match &self.app_state { AppState::Chat { widget } => widget.desired_height(size.width), AppState::Onboarding { .. } => size.height, - AppState::GitWarning { .. } => size.height, }; let mut area = terminal.viewport_area; @@ -507,7 +492,6 @@ impl App<'_> { frame.render_widget_ref(&**widget, frame.area()) } AppState::Onboarding { screen } => frame.render_widget_ref(&*screen, frame.area()), - AppState::GitWarning { screen } => frame.render_widget_ref(&*screen, frame.area()), })?; Ok(()) } @@ -519,49 +503,11 @@ impl App<'_> { AppState::Chat { widget } => { widget.handle_key_event(key_event); } - AppState::Onboarding { screen } => match screen.handle_key_event(key_event) { - KeyEventResult::Continue => { - self.app_state = AppState::Chat { - widget: Box::new(ChatWidget::new( - self.config.clone(), - self.app_event_tx.clone(), - None, - Vec::new(), - self.enhanced_keys_supported, - )), - }; - } - KeyEventResult::Quit => { + AppState::Onboarding { screen } => match key_event.code { + KeyCode::Char('q') => { self.app_event_tx.send(AppEvent::ExitRequest); } - KeyEventResult::None => { - // do nothing - } - }, - AppState::GitWarning { screen } => match screen.handle_key_event(key_event) { - GitWarningOutcome::Continue => { - // User accepted – switch to chat view. - let args = match self.chat_args.take() { - Some(args) => args, - None => panic!("ChatWidgetArgs already consumed"), - }; - - let widget = Box::new(ChatWidget::new( - args.config, - self.app_event_tx.clone(), - args.initial_prompt, - args.initial_images, - args.enhanced_keys_supported, - )); - self.app_state = AppState::Chat { widget }; - self.app_event_tx.send(AppEvent::RequestRedraw); - } - GitWarningOutcome::Quit => { - self.app_event_tx.send(AppEvent::ExitRequest); - } - GitWarningOutcome::None => { - // do nothing - } + _ => screen.handle_key_event(key_event), }, } } @@ -570,7 +516,6 @@ impl App<'_> { match &mut self.app_state { AppState::Chat { widget } => widget.handle_paste(pasted), AppState::Onboarding { .. } => {} - AppState::GitWarning { .. } => {} } } @@ -578,7 +523,6 @@ impl App<'_> { match &mut self.app_state { AppState::Chat { widget } => widget.handle_codex_event(event), AppState::Onboarding { .. } => {} - AppState::GitWarning { .. } => {} } } } diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 7df7761ac5..7f96fe1e47 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -3,6 +3,7 @@ use codex_file_search::FileMatch; use crossterm::event::KeyEvent; use ratatui::text::Line; +use crate::app::ChatWidgetArgs; use crate::slash_command::SlashCommand; #[allow(clippy::large_enum_variant)] @@ -51,4 +52,5 @@ pub(crate) enum AppEvent { /// Onboarding: result of login_with_chatgpt. OnboardingAuthComplete(Result<(), String>), + OnboardingComplete(ChatWidgetArgs), } diff --git a/codex-rs/tui/src/app_event_sender.rs b/codex-rs/tui/src/app_event_sender.rs index 9d838273ef..f6c8c18c98 100644 --- a/codex-rs/tui/src/app_event_sender.rs +++ b/codex-rs/tui/src/app_event_sender.rs @@ -4,7 +4,7 @@ use crate::app_event::AppEvent; #[derive(Clone, Debug)] pub(crate) struct AppEventSender { - app_event_tx: Sender, + pub app_event_tx: Sender, } impl AppEventSender { diff --git a/codex-rs/tui/src/git_warning_screen.rs b/codex-rs/tui/src/git_warning_screen.rs deleted file mode 100644 index 3a7ea21159..0000000000 --- a/codex-rs/tui/src/git_warning_screen.rs +++ /dev/null @@ -1,122 +0,0 @@ -//! Full‑screen warning displayed when Codex is started outside a Git -//! repository (unless the user passed `--allow-no-git-exec`). The screen -//! blocks all input until the user explicitly decides whether to continue or -//! quit. - -use crossterm::event::KeyCode; -use crossterm::event::KeyEvent; -use ratatui::buffer::Buffer; -use ratatui::layout::Alignment; -use ratatui::layout::Constraint; -use ratatui::layout::Direction; -use ratatui::layout::Layout; -use ratatui::layout::Rect; -use ratatui::style::Color; -use ratatui::style::Modifier; -use ratatui::style::Style; -use ratatui::text::Span; -use ratatui::widgets::Block; -use ratatui::widgets::BorderType; -use ratatui::widgets::Borders; -use ratatui::widgets::Paragraph; -use ratatui::widgets::Widget; -use ratatui::widgets::WidgetRef; -use ratatui::widgets::Wrap; - -const NO_GIT_ERROR: &str = "We recommend running codex inside a git repository. \ -This helps ensure that changes can be tracked and easily rolled back if necessary. \ -Do you wish to proceed?"; - -/// Result of handling a key event while the warning screen is active. -pub(crate) enum GitWarningOutcome { - /// User chose to proceed – switch to the main Chat UI. - Continue, - /// User opted to quit the application. - Quit, - /// No actionable key was pressed – stay on the warning screen. - None, -} - -pub(crate) struct GitWarningScreen; - -impl GitWarningScreen { - pub(crate) fn new() -> Self { - Self - } - - /// Handle a key event, returning an outcome indicating whether the user - /// chose to continue, quit, or neither. - pub(crate) fn handle_key_event(&self, key_event: KeyEvent) -> GitWarningOutcome { - match key_event.code { - KeyCode::Char('y') | KeyCode::Char('Y') => GitWarningOutcome::Continue, - KeyCode::Char('n') | KeyCode::Char('q') | KeyCode::Esc => GitWarningOutcome::Quit, - _ => GitWarningOutcome::None, - } - } -} - -impl WidgetRef for &GitWarningScreen { - fn render_ref(&self, area: Rect, buf: &mut Buffer) { - const MIN_WIDTH: u16 = 35; - const MIN_HEIGHT: u16 = 15; - // Check if the available area is too small for our popup. - if area.width < MIN_WIDTH || area.height < MIN_HEIGHT { - // Fallback rendering: a simple abbreviated message that fits the available area. - let fallback_message = Paragraph::new(NO_GIT_ERROR) - .wrap(Wrap { trim: true }) - .alignment(Alignment::Center); - fallback_message.render(area, buf); - return; - } - - // Determine the popup (modal) size – aim for 60 % width, 30 % height - // but keep a sensible minimum so the content is always readable. - let popup_width = std::cmp::max(MIN_WIDTH, (area.width as f32 * 0.6) as u16); - let popup_height = std::cmp::max(MIN_HEIGHT, (area.height as f32 * 0.3) as u16); - - // Center the popup in the available area. - let popup_x = area.x + (area.width.saturating_sub(popup_width)) / 2; - let popup_y = area.y + (area.height.saturating_sub(popup_height)) / 2; - let popup_area = Rect::new(popup_x, popup_y, popup_width, popup_height); - - // The modal block that contains everything. - let popup_block = Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Plain) - .title(Span::styled( - "Warning: Not a Git repository", // bold warning title - Style::default().add_modifier(Modifier::BOLD).fg(Color::Red), - )); - - // Obtain the inner area before rendering (render consumes the block). - let inner = popup_block.inner(popup_area); - popup_block.render(popup_area, buf); - - // Split the inner area vertically into two boxes: one for the warning - // explanation, one for the user action instructions. - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(3), Constraint::Length(3)]) - .split(inner); - - // ----- First box: detailed warning text -------------------------------- - let text_block = Block::default().borders(Borders::ALL); - let text_inner = text_block.inner(chunks[0]); - text_block.render(chunks[0], buf); - - let warning_paragraph = Paragraph::new(NO_GIT_ERROR) - .wrap(Wrap { trim: true }) - .alignment(Alignment::Left); - warning_paragraph.render(text_inner, buf); - - // ----- Second box: "proceed? y/n" instructions -------------------------- - let action_block = Block::default().borders(Borders::ALL); - let action_inner = action_block.inner(chunks[1]); - action_block.render(chunks[1], buf); - - let action_text = Paragraph::new("press 'y' to continue, 'n' to quit") - .alignment(Alignment::Center) - .style(Style::default().add_modifier(Modifier::BOLD)); - action_text.render(action_inner, buf); - } -} diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index e65083bfe1..0e809afdbe 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -8,7 +8,6 @@ use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_core::config_types::SandboxMode; use codex_core::protocol::AskForApproval; -use codex_core::util::is_inside_git_repo; use codex_login::load_auth; use codex_ollama::DEFAULT_OSS_MODEL; use log_layer::TuiLogLayer; @@ -31,7 +30,6 @@ pub mod custom_terminal; mod exec_command; mod file_search; mod get_git_diff; -mod git_warning_screen; mod history_cell; pub mod insert_history; pub mod live_wrap; @@ -206,20 +204,12 @@ pub async fn run_main( eprintln!(""); } - // Determine whether we need to display the "not a git repo" warning - // modal. The flag is shown when the current working directory is *not* - // inside a Git repository **and** the user did *not* pass the - // `--allow-no-git-exec` flag. - let show_git_warning = !cli.skip_git_repo_check && !is_inside_git_repo(&config); - - run_ratatui_app(cli, config, show_git_warning, log_rx) - .map_err(|err| std::io::Error::other(err.to_string())) + run_ratatui_app(cli, config, log_rx).map_err(|err| std::io::Error::other(err.to_string())) } fn run_ratatui_app( cli: Cli, config: Config, - show_git_warning: bool, mut log_rx: tokio::sync::mpsc::UnboundedReceiver, ) -> color_eyre::Result { color_eyre::install()?; @@ -237,7 +227,7 @@ fn run_ratatui_app( terminal.clear()?; let Cli { prompt, images, .. } = cli; - let mut app = App::new(config.clone(), prompt, show_git_warning, images); + let mut app = App::new(config.clone(), prompt, cli.skip_git_repo_check, images); // Bridge log receiver into the AppEvent channel so latest log lines update the UI. { diff --git a/codex-rs/tui/src/onboarding/auth.rs b/codex-rs/tui/src/onboarding/auth.rs index 834c36819e..b91bf4a0d9 100644 --- a/codex-rs/tui/src/onboarding/auth.rs +++ b/codex-rs/tui/src/onboarding/auth.rs @@ -18,18 +18,23 @@ use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; use crate::colors::LIGHT_BLUE; use crate::colors::SUCCESS_GREEN; -use crate::onboarding::onboarding_screen::KeyEventResult; use crate::onboarding::onboarding_screen::KeyboardHandler; +use crate::onboarding::onboarding_screen::StepStateProvider; use crate::shimmer::FrameTicker; use crate::shimmer::shimmer_spans; use std::path::PathBuf; + +use super::onboarding_screen::StepState; // no additional imports #[derive(Debug)] pub(crate) enum SignInState { PickMode, ChatGptContinueInBrowser(#[allow(dead_code)] ContinueInBrowserState), + ChatGptSuccessMessage, ChatGptSuccess, + EnvVarMissing, + EnvVarFound, } #[derive(Debug)] @@ -38,7 +43,6 @@ pub(crate) struct ContinueInBrowserState { _login_child: Option, _frame_ticker: Option, } - impl Drop for ContinueInBrowserState { fn drop(&mut self) { if let Some(child) = &self._login_child { @@ -52,54 +56,45 @@ impl Drop for ContinueInBrowserState { } impl KeyboardHandler for AuthModeWidget { - fn handle_key_event(&mut self, key_event: KeyEvent) -> KeyEventResult { + fn handle_key_event(&mut self, key_event: KeyEvent) { match key_event.code { KeyCode::Up | KeyCode::Char('k') => { - self.mode = AuthMode::ChatGPT; - KeyEventResult::None + self.highlighted_mode = AuthMode::ChatGPT; } KeyCode::Down | KeyCode::Char('j') => { - self.mode = AuthMode::ApiKey; - KeyEventResult::None + self.highlighted_mode = AuthMode::ApiKey; } KeyCode::Char('1') => { - self.mode = AuthMode::ChatGPT; self.start_chatgpt_login(); - KeyEventResult::None } - KeyCode::Char('2') => { - self.mode = AuthMode::ApiKey; - self.verify_api_key() - } - KeyCode::Enter => match self.mode { - AuthMode::ChatGPT => match &self.sign_in_state { - SignInState::PickMode => self.start_chatgpt_login(), - SignInState::ChatGptContinueInBrowser(_) => KeyEventResult::None, - SignInState::ChatGptSuccess => KeyEventResult::Continue, + KeyCode::Char('2') => self.verify_api_key(), + KeyCode::Enter => match self.sign_in_state { + SignInState::PickMode => match self.highlighted_mode { + AuthMode::ChatGPT => self.start_chatgpt_login(), + AuthMode::ApiKey => self.verify_api_key(), }, - AuthMode::ApiKey => self.verify_api_key(), + SignInState::EnvVarMissing => self.sign_in_state = SignInState::PickMode, + SignInState::ChatGptSuccessMessage => { + self.sign_in_state = SignInState::ChatGptSuccess + } + _ => {} }, KeyCode::Esc => { if matches!(self.sign_in_state, SignInState::ChatGptContinueInBrowser(_)) { self.sign_in_state = SignInState::PickMode; - self.event_tx.send(AppEvent::RequestRedraw); - KeyEventResult::None - } else { - KeyEventResult::Quit } } - KeyCode::Char('q') => KeyEventResult::Quit, - _ => KeyEventResult::None, + _ => {} } } } #[derive(Debug)] pub(crate) struct AuthModeWidget { - pub mode: AuthMode, + pub event_tx: AppEventSender, + pub highlighted_mode: AuthMode, pub error: Option, pub sign_in_state: SignInState, - pub event_tx: AppEventSender, pub codex_home: PathBuf, } @@ -121,7 +116,7 @@ impl AuthModeWidget { text: &str, description: &str| -> Vec> { - let is_selected = self.mode == selected_mode; + let is_selected = self.highlighted_mode == selected_mode; let caret = if is_selected { ">" } else { " " }; let line1 = if is_selected { @@ -192,7 +187,7 @@ impl AuthModeWidget { .render(area, buf); } - fn render_chatgpt_success(&self, area: Rect, buf: &mut Buffer) { + fn render_chatgpt_success_message(&self, area: Rect, buf: &mut Buffer) { let lines = vec![ Line::from("✓ Signed in with your ChatGPT account") .style(Style::default().fg(SUCCESS_GREEN)), @@ -219,7 +214,40 @@ impl AuthModeWidget { .render(area, buf); } - fn start_chatgpt_login(&mut self) -> KeyEventResult { + fn render_chatgpt_success(&self, area: Rect, buf: &mut Buffer) { + let lines = vec![ + Line::from("✓ Signed in with your ChatGPT account") + .style(Style::default().fg(SUCCESS_GREEN)), + ]; + + Paragraph::new(lines) + .wrap(Wrap { trim: false }) + .render(area, buf); + } + + fn render_env_var_found(&self, area: Rect, buf: &mut Buffer) { + let lines = + vec![Line::from("✓ Using OPENAI_API_KEY").style(Style::default().fg(SUCCESS_GREEN))]; + + Paragraph::new(lines) + .wrap(Wrap { trim: false }) + .render(area, buf); + } + + fn render_env_var_missing(&self, area: Rect, buf: &mut Buffer) { + let lines = vec![ + Line::from("✘ OPENAI_API_KEY not found").style(Style::default().fg(Color::Red)), + Line::from(""), + Line::from(" Press Enter to return") + .style(Style::default().add_modifier(Modifier::DIM)), + ]; + + Paragraph::new(lines) + .wrap(Wrap { trim: false }) + .render(area, buf); + } + + fn start_chatgpt_login(&mut self) { self.error = None; match codex_login::spawn_login_with_chatgpt(&self.codex_home) { Ok(child) => { @@ -230,27 +258,23 @@ impl AuthModeWidget { _frame_ticker: Some(FrameTicker::new(self.event_tx.clone())), }); self.event_tx.send(AppEvent::RequestRedraw); - KeyEventResult::None } Err(e) => { self.sign_in_state = SignInState::PickMode; self.error = Some(e.to_string()); self.event_tx.send(AppEvent::RequestRedraw); - KeyEventResult::None } } } /// TODO: Read/write from the correct hierarchy config overrides + auth json + OPENAI_API_KEY. - fn verify_api_key(&mut self) -> KeyEventResult { + fn verify_api_key(&mut self) { if std::env::var("OPENAI_API_KEY").is_err() { - self.error = - Some("Set OPENAI_API_KEY in your environment. Learn more: https://platform.openai.com/docs/libraries".to_string()); - self.event_tx.send(AppEvent::RequestRedraw); - KeyEventResult::None + self.sign_in_state = SignInState::EnvVarMissing; } else { - KeyEventResult::Continue + self.sign_in_state = SignInState::EnvVarFound; } + self.event_tx.send(AppEvent::RequestRedraw); } fn spawn_completion_poller(&self, child: codex_login::SpawnedLogin) { @@ -299,6 +323,18 @@ impl AuthModeWidget { } } +impl StepStateProvider for AuthModeWidget { + fn get_step_state(&self) -> StepState { + match &self.sign_in_state { + SignInState::PickMode + | SignInState::EnvVarMissing + | SignInState::ChatGptContinueInBrowser(_) + | SignInState::ChatGptSuccessMessage => StepState::InProgress, + SignInState::ChatGptSuccess | SignInState::EnvVarFound => StepState::Complete, + } + } +} + impl WidgetRef for AuthModeWidget { fn render_ref(&self, area: Rect, buf: &mut Buffer) { match self.sign_in_state { @@ -308,9 +344,18 @@ impl WidgetRef for AuthModeWidget { SignInState::ChatGptContinueInBrowser(_) => { self.render_continue_in_browser(area, buf); } + SignInState::ChatGptSuccessMessage => { + self.render_chatgpt_success_message(area, buf); + } SignInState::ChatGptSuccess => { self.render_chatgpt_success(area, buf); } + SignInState::EnvVarMissing => { + self.render_env_var_missing(area, buf); + } + SignInState::EnvVarFound => { + self.render_env_var_found(area, buf); + } } } } diff --git a/codex-rs/tui/src/onboarding/continue_to_chat.rs b/codex-rs/tui/src/onboarding/continue_to_chat.rs new file mode 100644 index 0000000000..071d0851da --- /dev/null +++ b/codex-rs/tui/src/onboarding/continue_to_chat.rs @@ -0,0 +1,30 @@ +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::widgets::WidgetRef; + +use crate::app::ChatWidgetArgs; +use crate::app_event::AppEvent; +use crate::app_event_sender::AppEventSender; +use crate::onboarding::onboarding_screen::StepStateProvider; + +use super::onboarding_screen::StepState; + +/// This doesn't render anything explicitly but serves as a signal that we made it to the end and +/// we should continue to the chat. +pub(crate) struct ContinueToChatWidget { + pub event_tx: AppEventSender, + pub chat_widget_args: ChatWidgetArgs, +} + +impl StepStateProvider for ContinueToChatWidget { + fn get_step_state(&self) -> StepState { + StepState::Complete + } +} + +impl WidgetRef for &ContinueToChatWidget { + fn render_ref(&self, _area: Rect, _buf: &mut Buffer) { + self.event_tx + .send(AppEvent::OnboardingComplete(self.chat_widget_args.clone())); + } +} diff --git a/codex-rs/tui/src/onboarding/git_warning.rs b/codex-rs/tui/src/onboarding/git_warning.rs new file mode 100644 index 0000000000..e4e5747404 --- /dev/null +++ b/codex-rs/tui/src/onboarding/git_warning.rs @@ -0,0 +1,126 @@ +use std::path::PathBuf; + +use codex_core::util::is_inside_git_repo; +use crossterm::event::KeyCode; +use crossterm::event::KeyEvent; +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::prelude::Widget; +use ratatui::style::Modifier; +use ratatui::style::Style; +use ratatui::style::Stylize; +use ratatui::text::Line; +use ratatui::text::Span; +use ratatui::widgets::Paragraph; +use ratatui::widgets::WidgetRef; +use ratatui::widgets::Wrap; + +use crate::app_event::AppEvent; +use crate::app_event_sender::AppEventSender; +use crate::colors::LIGHT_BLUE; + +use crate::onboarding::onboarding_screen::KeyboardHandler; +use crate::onboarding::onboarding_screen::StepStateProvider; + +use super::onboarding_screen::StepState; + +pub(crate) struct GitWarningWidget { + pub event_tx: AppEventSender, + pub cwd: PathBuf, + pub selection: Option, + pub highlighted: GitWarningSelection, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum GitWarningSelection { + Continue, + Exit, +} + +impl WidgetRef for &GitWarningWidget { + fn render_ref(&self, area: Rect, buf: &mut Buffer) { + let mut lines: Vec = vec![ + Line::from(vec![ + Span::raw("> "), + Span::raw("You are running Codex in "), + Span::styled( + self.cwd.to_string_lossy().to_string(), + Style::default().add_modifier(Modifier::BOLD), + ), + Span::raw(". This folder is not version controlled."), + ]), + Line::from(""), + Line::from(" Do you want to continue?"), + Line::from(""), + ]; + + let create_option = + |idx: usize, option: GitWarningSelection, text: &str| -> Line<'static> { + let is_selected = self.highlighted == option; + if is_selected { + Line::from(vec![ + Span::styled( + format!("> {}. ", idx + 1), + Style::default().fg(LIGHT_BLUE).add_modifier(Modifier::DIM), + ), + Span::styled(text.to_owned(), Style::default().fg(LIGHT_BLUE)), + ]) + } else { + Line::from(format!(" {}. {}", idx + 1, text)) + } + }; + + lines.push(create_option(0, GitWarningSelection::Continue, "Yes")); + lines.push(create_option(1, GitWarningSelection::Exit, "No")); + lines.push(Line::from("")); + lines.push(Line::from(" Press Enter to continue").add_modifier(Modifier::DIM)); + + Paragraph::new(lines) + .wrap(Wrap { trim: false }) + .render(area, buf); + } +} + +impl KeyboardHandler for GitWarningWidget { + fn handle_key_event(&mut self, key_event: KeyEvent) { + match key_event.code { + KeyCode::Up | KeyCode::Char('k') => { + self.highlighted = GitWarningSelection::Continue; + } + KeyCode::Down | KeyCode::Char('j') => { + self.highlighted = GitWarningSelection::Exit; + } + KeyCode::Char('1') => self.handle_continue(), + KeyCode::Char('2') => self.handle_quit(), + KeyCode::Enter => match self.highlighted { + GitWarningSelection::Continue => self.handle_continue(), + GitWarningSelection::Exit => self.handle_quit(), + }, + _ => {} + } + } +} + +impl StepStateProvider for GitWarningWidget { + fn get_step_state(&self) -> StepState { + let is_git_repo = is_inside_git_repo(&self.cwd); + match is_git_repo { + true => StepState::Hidden, + false => match self.selection { + Some(_) => StepState::Complete, + None => StepState::InProgress, + }, + } + } +} + +impl GitWarningWidget { + fn handle_continue(&mut self) { + self.selection = Some(GitWarningSelection::Continue); + } + + fn handle_quit(&mut self) { + self.highlighted = GitWarningSelection::Exit; + self.event_tx.send(AppEvent::ExitRequest); + } +} diff --git a/codex-rs/tui/src/onboarding/mod.rs b/codex-rs/tui/src/onboarding/mod.rs index 42d3ac8187..645cda22d9 100644 --- a/codex-rs/tui/src/onboarding/mod.rs +++ b/codex-rs/tui/src/onboarding/mod.rs @@ -1,3 +1,5 @@ mod auth; +mod continue_to_chat; +mod git_warning; pub mod onboarding_screen; mod welcome; diff --git a/codex-rs/tui/src/onboarding/onboarding_screen.rs b/codex-rs/tui/src/onboarding/onboarding_screen.rs index e2548bacb3..7ce7d16c47 100644 --- a/codex-rs/tui/src/onboarding/onboarding_screen.rs +++ b/codex-rs/tui/src/onboarding/onboarding_screen.rs @@ -5,26 +5,37 @@ use ratatui::widgets::WidgetRef; use codex_login::AuthMode; +use crate::app::ChatWidgetArgs; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; use crate::onboarding::auth::AuthModeWidget; use crate::onboarding::auth::SignInState; +use crate::onboarding::continue_to_chat::ContinueToChatWidget; +use crate::onboarding::git_warning::GitWarningSelection; +use crate::onboarding::git_warning::GitWarningWidget; use crate::onboarding::welcome::WelcomeWidget; use std::path::PathBuf; +#[allow(clippy::large_enum_variant)] enum Step { Welcome(WelcomeWidget), Auth(AuthModeWidget), + GitWarning(GitWarningWidget), + ContinueToChat(ContinueToChatWidget), } pub(crate) trait KeyboardHandler { - fn handle_key_event(&mut self, key_event: KeyEvent) -> KeyEventResult; + fn handle_key_event(&mut self, key_event: KeyEvent); } -pub(crate) enum KeyEventResult { - Continue, - Quit, - None, +pub(crate) enum StepState { + Hidden, + InProgress, + Complete, +} + +pub(crate) trait StepStateProvider { + fn get_step_state(&self) -> StepState; } pub(crate) struct OnboardingScreen { @@ -32,50 +43,113 @@ pub(crate) struct OnboardingScreen { steps: Vec, } +pub(crate) struct OnboardingScreenArgs { + pub event_tx: AppEventSender, + pub chat_widget_args: ChatWidgetArgs, + pub codex_home: PathBuf, + pub cwd: PathBuf, + pub show_login_screen: bool, + pub show_git_warning: bool, +} + impl OnboardingScreen { - pub(crate) fn new(event_tx: AppEventSender, codex_home: PathBuf) -> Self { - let steps: Vec = vec![ - Step::Welcome(WelcomeWidget {}), - Step::Auth(AuthModeWidget { + pub(crate) fn new(args: OnboardingScreenArgs) -> Self { + let OnboardingScreenArgs { + event_tx, + chat_widget_args, + codex_home, + cwd, + show_login_screen, + show_git_warning, + } = args; + let mut steps: Vec = vec![Step::Welcome(WelcomeWidget { + is_logged_in: !show_login_screen, + })]; + if show_login_screen { + steps.push(Step::Auth(AuthModeWidget { event_tx: event_tx.clone(), - mode: AuthMode::ChatGPT, + highlighted_mode: AuthMode::ChatGPT, error: None, sign_in_state: SignInState::PickMode, codex_home, - }), - ]; + })) + } + if show_git_warning { + steps.push(Step::GitWarning(GitWarningWidget { + event_tx: event_tx.clone(), + cwd, + selection: None, + highlighted: GitWarningSelection::Continue, + })) + } + steps.push(Step::ContinueToChat(ContinueToChatWidget { + event_tx: event_tx.clone(), + chat_widget_args, + })); + // TODO: add git warning. Self { event_tx, steps } } - pub(crate) fn on_auth_complete(&mut self, result: Result<(), String>) -> KeyEventResult { - if let Some(Step::Auth(state)) = self.steps.last_mut() { + pub(crate) fn on_auth_complete(&mut self, result: Result<(), String>) { + let current_step = self.current_step_mut(); + if let Some(Step::Auth(state)) = current_step { match result { Ok(()) => { - state.sign_in_state = SignInState::ChatGptSuccess; + state.sign_in_state = SignInState::ChatGptSuccessMessage; self.event_tx.send(AppEvent::RequestRedraw); - KeyEventResult::None } Err(e) => { state.sign_in_state = SignInState::PickMode; state.error = Some(e); self.event_tx.send(AppEvent::RequestRedraw); - KeyEventResult::None } } - } else { - KeyEventResult::None } } + + fn current_steps_mut(&mut self) -> Vec<&mut Step> { + let mut out: Vec<&mut Step> = Vec::new(); + for step in self.steps.iter_mut() { + match step.get_step_state() { + StepState::Hidden => continue, + StepState::Complete => out.push(step), + StepState::InProgress => { + out.push(step); + break; + } + } + } + out + } + + fn current_steps(&self) -> Vec<&Step> { + let mut out: Vec<&Step> = Vec::new(); + for step in self.steps.iter() { + match step.get_step_state() { + StepState::Hidden => continue, + StepState::Complete => out.push(step), + StepState::InProgress => { + out.push(step); + break; + } + } + } + out + } + + fn current_step_mut(&mut self) -> Option<&mut Step> { + self.steps + .iter_mut() + .find(|step| matches!(step.get_step_state(), StepState::InProgress)) + } } impl KeyboardHandler for OnboardingScreen { - fn handle_key_event(&mut self, key_event: KeyEvent) -> KeyEventResult { - if let Some(last_step) = self.steps.last_mut() { - self.event_tx.send(AppEvent::RequestRedraw); - last_step.handle_key_event(key_event) - } else { - KeyEventResult::None + fn handle_key_event(&mut self, key_event: KeyEvent) { + if let Some(active_step) = self.current_steps_mut().into_iter().last() { + active_step.handle_key_event(key_event); } + self.event_tx.send(AppEvent::RequestRedraw); } } @@ -109,8 +183,10 @@ impl WidgetRef for &OnboardingScreen { } let mut i = 0usize; - while i < self.steps.len() && y < bottom { - let step = &self.steps[i]; + let current_steps = self.current_steps(); + + while i < current_steps.len() && y < bottom { + let step = ¤t_steps[i]; let max_h = bottom.saturating_sub(y); if max_h == 0 || width == 0 { break; @@ -135,10 +211,22 @@ impl WidgetRef for &OnboardingScreen { } impl KeyboardHandler for Step { - fn handle_key_event(&mut self, key_event: KeyEvent) -> KeyEventResult { + fn handle_key_event(&mut self, key_event: KeyEvent) { match self { - Step::Welcome(_) => KeyEventResult::None, + Step::Welcome(_) | Step::ContinueToChat(_) => (), Step::Auth(widget) => widget.handle_key_event(key_event), + Step::GitWarning(widget) => widget.handle_key_event(key_event), + } + } +} + +impl StepStateProvider for Step { + fn get_step_state(&self) -> StepState { + match self { + Step::Welcome(w) => w.get_step_state(), + Step::Auth(w) => w.get_step_state(), + Step::GitWarning(w) => w.get_step_state(), + Step::ContinueToChat(w) => w.get_step_state(), } } } @@ -152,6 +240,12 @@ impl WidgetRef for Step { Step::Auth(widget) => { widget.render_ref(area, buf); } + Step::GitWarning(widget) => { + widget.render_ref(area, buf); + } + Step::ContinueToChat(widget) => { + widget.render_ref(area, buf); + } } } } diff --git a/codex-rs/tui/src/onboarding/welcome.rs b/codex-rs/tui/src/onboarding/welcome.rs index e00e3004b6..a35f6528ab 100644 --- a/codex-rs/tui/src/onboarding/welcome.rs +++ b/codex-rs/tui/src/onboarding/welcome.rs @@ -7,7 +7,13 @@ use ratatui::text::Line; use ratatui::text::Span; use ratatui::widgets::WidgetRef; -pub(crate) struct WelcomeWidget {} +use crate::onboarding::onboarding_screen::StepStateProvider; + +use super::onboarding_screen::StepState; + +pub(crate) struct WelcomeWidget { + pub is_logged_in: bool, +} impl WidgetRef for &WelcomeWidget { fn render_ref(&self, area: Rect, buf: &mut Buffer) { @@ -21,3 +27,12 @@ impl WidgetRef for &WelcomeWidget { line.render(area, buf); } } + +impl StepStateProvider for WelcomeWidget { + fn get_step_state(&self) -> StepState { + match self.is_logged_in { + true => StepState::Hidden, + false => StepState::Complete, + } + } +} From 4971d54ca7b193dfb66b634c8f9dc9185a1dbb53 Mon Sep 17 00:00:00 2001 From: aibrahim-oai Date: Wed, 6 Aug 2025 21:20:09 -0700 Subject: [PATCH 0058/1309] Show timing and token counts in status indicator (#1909) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - track start time and cumulative tokens in status indicator - display dim "(Ns • N tokens • Ctrl z to interrupt)" text after animated Working header - propagate token usage updates to status indicator views https://github.com/user-attachments/assets/b73210c1-1533-40b5-b6c2-3c640029fd54 ## Testing - `just fmt` - `just fix` *(fails: let expressions in this position are unstable)* - `cargo test --all-features` *(fails: let expressions in this position are unstable)* ------ https://chatgpt.com/codex/tasks/task_i_6893ec0d74a883218b94005172d7bc4c --- codex-rs/tui/src/status_indicator_widget.rs | 56 +++++++++++++++------ 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/codex-rs/tui/src/status_indicator_widget.rs b/codex-rs/tui/src/status_indicator_widget.rs index fad7e41a39..8cca0fb04d 100644 --- a/codex-rs/tui/src/status_indicator_widget.rs +++ b/codex-rs/tui/src/status_indicator_widget.rs @@ -7,6 +7,7 @@ use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; use std::thread; use std::time::Duration; +use std::time::Instant; use ratatui::buffer::Buffer; use ratatui::layout::Rect; @@ -42,6 +43,7 @@ pub(crate) struct StatusIndicatorWidget { frame_idx: Arc, running: Arc, + start_time: Instant, // Keep one sender alive to prevent the channel from closing while the // animation thread is still running. The field itself is currently not // accessed anywhere, therefore the leading underscore silences the @@ -78,6 +80,7 @@ impl StatusIndicatorWidget { reveal_len_at_base: 0, frame_idx, running, + start_time: Instant::now(), _app_event_tx: app_event_tx, } @@ -167,11 +170,13 @@ impl WidgetRef for StatusIndicatorWidget { return; } - // Build animated gradient header for the word "Working". let idx = self.frame_idx.load(std::sync::atomic::Ordering::Relaxed); - let header_text = "Working"; - let header_chars: Vec = header_text.chars().collect(); - let padding = 4usize; // virtual padding around the word for smoother loop + let elapsed = self.start_time.elapsed().as_secs(); + let shown_now = self.current_shown_len(idx); + let status_prefix: String = self.text.chars().take(shown_now).collect(); + let animated_text = "Working"; + let header_chars: Vec = animated_text.chars().collect(); + let padding = 4usize; // virtual padding around the animated segment for smoother loop let period = header_chars.len() + padding * 2; let pos = idx % period; let has_true_color = supports_color::on_cached(supports_color::Stream::Stdout) @@ -179,7 +184,7 @@ impl WidgetRef for StatusIndicatorWidget { .unwrap_or(false); let band_half_width = 2.0; // width of the bright band in characters - let mut header_spans: Vec> = Vec::new(); + let mut animated_spans: Vec> = Vec::new(); for (i, ch) in header_chars.iter().enumerate() { let i_pos = i as isize + padding as isize; let pos = pos as isize; @@ -199,28 +204,49 @@ impl WidgetRef for StatusIndicatorWidget { .fg(Color::Rgb(level, level, level)) .add_modifier(Modifier::BOLD) } else { - // Bold makes dark gray and gray look the same, so don't use it when true color is not supported. Style::default().fg(color_for_level(level)) }; - header_spans.push(Span::styled(ch.to_string(), style)); + animated_spans.push(Span::styled(ch.to_string(), style)); } // Plain rendering: no borders or padding so the live cell is visually indistinguishable from terminal scrollback. let inner_width = area.width as usize; - // Compose a single status line like: "▌ Working [•] waiting for model" + // Compose a single status line like: "▌ Working (Xs • Ctrl z to interrupt) " let mut spans: Vec> = Vec::new(); spans.push(Span::styled("▌ ", Style::default().fg(Color::Cyan))); - // Gradient header - spans.extend(header_spans); - // Space after header + // Animated header after the left bar + spans.extend(animated_spans); + // Space between header and bracket block + spans.push(Span::raw(" ")); + // Non-animated, dim bracket content, with only "Ctrl z" bold + let bracket_prefix = format!("({elapsed}s • "); spans.push(Span::styled( - " ", - Style::default() - .fg(Color::White) - .add_modifier(Modifier::BOLD), + bracket_prefix, + Style::default().fg(Color::Gray).add_modifier(Modifier::DIM), )); + spans.push(Span::styled( + "Ctrl z", + Style::default() + .fg(Color::Gray) + .add_modifier(Modifier::DIM | Modifier::BOLD), + )); + spans.push(Span::styled( + " to interrupt)", + Style::default().fg(Color::Gray).add_modifier(Modifier::DIM), + )); + // Add a space and then the log text (not animated by the gradient) + if !status_prefix.is_empty() { + spans.push(Span::styled( + " ", + Style::default().fg(Color::Gray).add_modifier(Modifier::DIM), + )); + spans.push(Span::styled( + status_prefix, + Style::default().fg(Color::Gray).add_modifier(Modifier::DIM), + )); + } // Truncate spans to fit the width. let mut acc: Vec> = Vec::new(); From 2098b4036994878fffb221459bebd96648375285 Mon Sep 17 00:00:00 2001 From: easong-openai Date: Wed, 6 Aug 2025 21:23:09 -0700 Subject: [PATCH 0059/1309] Scrollable slash commands (#1830) Scrollable slash commands. Part 1 of the multi PR. --- codex-rs/common/src/fuzzy_match.rs | 177 ++++++++++++++++++ codex-rs/common/src/lib.rs | 2 + codex-rs/tui/src/bottom_pane/command_popup.rs | 173 ++++++----------- .../tui/src/bottom_pane/file_search_popup.rs | 129 ++++--------- codex-rs/tui/src/bottom_pane/mod.rs | 3 + codex-rs/tui/src/bottom_pane/popup_consts.rs | 5 + codex-rs/tui/src/bottom_pane/scroll_state.rs | 115 ++++++++++++ .../src/bottom_pane/selection_popup_common.rs | 126 +++++++++++++ 8 files changed, 523 insertions(+), 207 deletions(-) create mode 100644 codex-rs/common/src/fuzzy_match.rs create mode 100644 codex-rs/tui/src/bottom_pane/popup_consts.rs create mode 100644 codex-rs/tui/src/bottom_pane/scroll_state.rs create mode 100644 codex-rs/tui/src/bottom_pane/selection_popup_common.rs diff --git a/codex-rs/common/src/fuzzy_match.rs b/codex-rs/common/src/fuzzy_match.rs new file mode 100644 index 0000000000..836848d6a4 --- /dev/null +++ b/codex-rs/common/src/fuzzy_match.rs @@ -0,0 +1,177 @@ +/// Simple case-insensitive subsequence matcher used for fuzzy filtering. +/// +/// Returns the indices (character positions) of the matched characters in the +/// ORIGINAL `haystack` string and a score where smaller is better. +/// +/// Unicode correctness: we perform the match on a lowercased copy of the +/// haystack and needle but maintain a mapping from each character in the +/// lowercased haystack back to the original character index in `haystack`. +/// This ensures the returned indices can be safely used with +/// `str::chars().enumerate()` consumers for highlighting, even when +/// lowercasing expands certain characters (e.g., ß → ss, İ → i̇). +pub fn fuzzy_match(haystack: &str, needle: &str) -> Option<(Vec, i32)> { + if needle.is_empty() { + return Some((Vec::new(), i32::MAX)); + } + + let mut lowered_chars: Vec = Vec::new(); + let mut lowered_to_orig_char_idx: Vec = Vec::new(); + for (orig_idx, ch) in haystack.chars().enumerate() { + for lc in ch.to_lowercase() { + lowered_chars.push(lc); + lowered_to_orig_char_idx.push(orig_idx); + } + } + + let lowered_needle: Vec = needle.to_lowercase().chars().collect(); + + let mut result_orig_indices: Vec = Vec::with_capacity(lowered_needle.len()); + let mut last_lower_pos: Option = None; + let mut cur = 0usize; + for &nc in lowered_needle.iter() { + let mut found_at: Option = None; + while cur < lowered_chars.len() { + if lowered_chars[cur] == nc { + found_at = Some(cur); + cur += 1; + break; + } + cur += 1; + } + let pos = found_at?; + result_orig_indices.push(lowered_to_orig_char_idx[pos]); + last_lower_pos = Some(pos); + } + + let first_lower_pos = if result_orig_indices.is_empty() { + 0usize + } else { + let target_orig = result_orig_indices[0]; + lowered_to_orig_char_idx + .iter() + .position(|&oi| oi == target_orig) + .unwrap_or(0) + }; + // last defaults to first for single-hit; score = extra span between first/last hit + // minus needle len (≥0). + // Strongly reward prefix matches by subtracting 100 when the first hit is at index 0. + let last_lower_pos = last_lower_pos.unwrap_or(first_lower_pos); + let window = + (last_lower_pos as i32 - first_lower_pos as i32 + 1) - (lowered_needle.len() as i32); + let mut score = window.max(0); + if first_lower_pos == 0 { + score -= 100; + } + + result_orig_indices.sort_unstable(); + result_orig_indices.dedup(); + Some((result_orig_indices, score)) +} + +/// Convenience wrapper to get only the indices for a fuzzy match. +pub fn fuzzy_indices(haystack: &str, needle: &str) -> Option> { + fuzzy_match(haystack, needle).map(|(mut idx, _)| { + idx.sort_unstable(); + idx.dedup(); + idx + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ascii_basic_indices() { + let (idx, score) = match fuzzy_match("hello", "hl") { + Some(v) => v, + None => panic!("expected a match"), + }; + assert_eq!(idx, vec![0, 2]); + // 'h' at 0, 'l' at 2 -> window 1; start-of-string bonus applies (-100) + assert_eq!(score, -99); + } + + #[test] + fn unicode_dotted_i_istanbul_highlighting() { + let (idx, score) = match fuzzy_match("İstanbul", "is") { + Some(v) => v, + None => panic!("expected a match"), + }; + assert_eq!(idx, vec![0, 1]); + // Matches at lowered positions 0 and 2 -> window 1; start-of-string bonus applies + assert_eq!(score, -99); + } + + #[test] + fn unicode_german_sharp_s_casefold() { + assert!(fuzzy_match("straße", "strasse").is_none()); + } + + #[test] + fn prefer_contiguous_match_over_spread() { + let (_idx_a, score_a) = match fuzzy_match("abc", "abc") { + Some(v) => v, + None => panic!("expected a match"), + }; + let (_idx_b, score_b) = match fuzzy_match("a-b-c", "abc") { + Some(v) => v, + None => panic!("expected a match"), + }; + // Contiguous window -> 0; start-of-string bonus -> -100 + assert_eq!(score_a, -100); + // Spread over 5 chars for 3-letter needle -> window 2; with bonus -> -98 + assert_eq!(score_b, -98); + assert!(score_a < score_b); + } + + #[test] + fn start_of_string_bonus_applies() { + let (_idx_a, score_a) = match fuzzy_match("file_name", "file") { + Some(v) => v, + None => panic!("expected a match"), + }; + let (_idx_b, score_b) = match fuzzy_match("my_file_name", "file") { + Some(v) => v, + None => panic!("expected a match"), + }; + // Start-of-string contiguous -> window 0; bonus -> -100 + assert_eq!(score_a, -100); + // Non-prefix contiguous -> window 0; no bonus -> 0 + assert_eq!(score_b, 0); + assert!(score_a < score_b); + } + + #[test] + fn empty_needle_matches_with_max_score_and_no_indices() { + let (idx, score) = match fuzzy_match("anything", "") { + Some(v) => v, + None => panic!("empty needle should match"), + }; + assert!(idx.is_empty()); + assert_eq!(score, i32::MAX); + } + + #[test] + fn case_insensitive_matching_basic() { + let (idx, score) = match fuzzy_match("FooBar", "foO") { + Some(v) => v, + None => panic!("expected a match"), + }; + assert_eq!(idx, vec![0, 1, 2]); + // Contiguous prefix match (case-insensitive) -> window 0 with bonus + assert_eq!(score, -100); + } + + #[test] + fn indices_are_deduped_for_multichar_lowercase_expansion() { + let needle = "\u{0069}\u{0307}"; // "i" + combining dot above + let (idx, score) = match fuzzy_match("İ", needle) { + Some(v) => v, + None => panic!("expected a match"), + }; + assert_eq!(idx, vec![0]); + // Lowercasing 'İ' expands to two chars; contiguous prefix -> window 0 with bonus + assert_eq!(score, -100); + } +} diff --git a/codex-rs/common/src/lib.rs b/codex-rs/common/src/lib.rs index 38f3832bfd..8595262cc0 100644 --- a/codex-rs/common/src/lib.rs +++ b/codex-rs/common/src/lib.rs @@ -27,3 +27,5 @@ pub use sandbox_summary::summarize_sandbox_policy; mod config_summary; pub use config_summary::create_config_summary_entries; +// Shared fuzzy matcher (used by TUI selection popups and other UI filtering) +pub mod fuzzy_match; diff --git a/codex-rs/tui/src/bottom_pane/command_popup.rs b/codex-rs/tui/src/bottom_pane/command_popup.rs index 1027df1a67..b7a203e9fd 100644 --- a/codex-rs/tui/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui/src/bottom_pane/command_popup.rs @@ -1,30 +1,19 @@ use ratatui::buffer::Buffer; use ratatui::layout::Rect; -use ratatui::style::Color; -use ratatui::style::Style; -use ratatui::style::Stylize; -use ratatui::symbols::border::QUADRANT_LEFT_HALF; -use ratatui::text::Line; -use ratatui::text::Span; -use ratatui::widgets::Cell; -use ratatui::widgets::Row; -use ratatui::widgets::Table; -use ratatui::widgets::Widget; use ratatui::widgets::WidgetRef; +use super::popup_consts::MAX_POPUP_ROWS; +use super::scroll_state::ScrollState; +use super::selection_popup_common::GenericDisplayRow; +use super::selection_popup_common::render_rows; use crate::slash_command::SlashCommand; use crate::slash_command::built_in_slash_commands; - -const MAX_POPUP_ROWS: usize = 5; -/// Ideally this is enough to show the longest command name. -const FIRST_COLUMN_WIDTH: u16 = 20; - -use ratatui::style::Modifier; +use codex_common::fuzzy_match::fuzzy_match; pub(crate) struct CommandPopup { command_filter: String, all_commands: Vec<(&'static str, SlashCommand)>, - selected_idx: Option, + state: ScrollState, } impl CommandPopup { @@ -32,7 +21,7 @@ impl CommandPopup { Self { command_filter: String::new(), all_commands: built_in_slash_commands(), - selected_idx: None, + state: ScrollState::new(), } } @@ -62,130 +51,84 @@ impl CommandPopup { // Reset or clamp selected index based on new filtered list. let matches_len = self.filtered_commands().len(); - self.selected_idx = match matches_len { - 0 => None, - _ => Some(self.selected_idx.unwrap_or(0).min(matches_len - 1)), - }; + self.state.clamp_selection(matches_len); + self.state + .ensure_visible(matches_len, MAX_POPUP_ROWS.min(matches_len)); } /// Determine the preferred height of the popup. This is the number of - /// rows required to show **at most** `MAX_POPUP_ROWS` commands plus the - /// table/border overhead (one line at the top and one at the bottom). + /// rows required to show at most MAX_POPUP_ROWS commands. pub(crate) fn calculate_required_height(&self) -> u16 { self.filtered_commands().len().clamp(1, MAX_POPUP_ROWS) as u16 } - /// Return the list of commands that match the current filter. Matching is - /// performed using a *prefix* comparison on the command name. - fn filtered_commands(&self) -> Vec<&SlashCommand> { - self.all_commands - .iter() - .filter_map(|(_name, cmd)| { - if self.command_filter.is_empty() - || cmd - .command() - .starts_with(&self.command_filter.to_ascii_lowercase()) - { - Some(cmd) - } else { - None + /// Compute fuzzy-filtered matches paired with optional highlight indices and score. + /// Sorted by ascending score, then by command name for stability. + fn filtered(&self) -> Vec<(&SlashCommand, Option>, i32)> { + let filter = self.command_filter.trim(); + let mut out: Vec<(&SlashCommand, Option>, i32)> = Vec::new(); + if filter.is_empty() { + for (_, cmd) in self.all_commands.iter() { + out.push((cmd, None, 0)); + } + } else { + for (_, cmd) in self.all_commands.iter() { + if let Some((indices, score)) = fuzzy_match(cmd.command(), filter) { + out.push((cmd, Some(indices), score)); } - }) - .collect::>() + } + } + out.sort_by(|a, b| a.2.cmp(&b.2).then_with(|| a.0.command().cmp(b.0.command()))); + out + } + + fn filtered_commands(&self) -> Vec<&SlashCommand> { + self.filtered().into_iter().map(|(c, _, _)| c).collect() } /// Move the selection cursor one step up. pub(crate) fn move_up(&mut self) { - if let Some(len) = self.filtered_commands().len().checked_sub(1) { - if len == usize::MAX { - return; - } - } - - if let Some(idx) = self.selected_idx { - if idx > 0 { - self.selected_idx = Some(idx - 1); - } - } else if !self.filtered_commands().is_empty() { - self.selected_idx = Some(0); - } + let matches = self.filtered_commands(); + let len = matches.len(); + self.state.move_up_wrap(len); + self.state.ensure_visible(len, MAX_POPUP_ROWS.min(len)); } /// Move the selection cursor one step down. pub(crate) fn move_down(&mut self) { - let matches_len = self.filtered_commands().len(); - if matches_len == 0 { - self.selected_idx = None; - return; - } - - match self.selected_idx { - Some(idx) if idx + 1 < matches_len => { - self.selected_idx = Some(idx + 1); - } - None => { - self.selected_idx = Some(0); - } - _ => {} - } + let matches = self.filtered_commands(); + let matches_len = matches.len(); + self.state.move_down_wrap(matches_len); + self.state + .ensure_visible(matches_len, MAX_POPUP_ROWS.min(matches_len)); } /// Return currently selected command, if any. pub(crate) fn selected_command(&self) -> Option<&SlashCommand> { let matches = self.filtered_commands(); - self.selected_idx.and_then(|idx| matches.get(idx).copied()) + self.state + .selected_idx + .and_then(|idx| matches.get(idx).copied()) } } impl WidgetRef for CommandPopup { fn render_ref(&self, area: Rect, buf: &mut Buffer) { - let matches = self.filtered_commands(); - - let mut rows: Vec = Vec::new(); - let visible_matches: Vec<&SlashCommand> = - matches.into_iter().take(MAX_POPUP_ROWS).collect(); - - if visible_matches.is_empty() { - rows.push(Row::new(vec![ - Cell::from(""), - Cell::from("No matching commands").add_modifier(Modifier::ITALIC), - ])); + let matches = self.filtered(); + let rows_all: Vec = if matches.is_empty() { + Vec::new() } else { - let default_style = Style::default(); - let command_style = Style::default().fg(Color::LightBlue); - for (idx, cmd) in visible_matches.iter().enumerate() { - rows.push(Row::new(vec![ - Cell::from(Line::from(vec![ - if Some(idx) == self.selected_idx { - Span::styled( - "›", - Style::default().bg(Color::DarkGray).fg(Color::LightCyan), - ) - } else { - Span::styled(QUADRANT_LEFT_HALF, Style::default().fg(Color::DarkGray)) - }, - Span::styled(format!("/{}", cmd.command()), command_style), - ])), - Cell::from(cmd.description().to_string()).style(default_style), - ])); - } - } - - use ratatui::layout::Constraint; - - let table = Table::new( - rows, - [Constraint::Length(FIRST_COLUMN_WIDTH), Constraint::Min(10)], - ) - .column_spacing(0); - // .block( - // Block::default() - // .borders(Borders::LEFT) - // .border_type(BorderType::QuadrantOutside) - // .border_style(Style::default().fg(Color::DarkGray)), - // ); - - table.render(area, buf); + matches + .into_iter() + .map(|(cmd, indices, _)| GenericDisplayRow { + name: format!("/{}", cmd.command()), + match_indices: indices.map(|v| v.into_iter().map(|i| i + 1).collect()), + is_current: false, + description: Some(cmd.description().to_string()), + }) + .collect() + }; + render_rows(area, buf, &rows_all, &self.state, MAX_POPUP_ROWS); } } diff --git a/codex-rs/tui/src/bottom_pane/file_search_popup.rs b/codex-rs/tui/src/bottom_pane/file_search_popup.rs index ac6c91cf47..c30a24f984 100644 --- a/codex-rs/tui/src/bottom_pane/file_search_popup.rs +++ b/codex-rs/tui/src/bottom_pane/file_search_popup.rs @@ -1,23 +1,12 @@ use codex_file_search::FileMatch; use ratatui::buffer::Buffer; use ratatui::layout::Rect; -use ratatui::prelude::Constraint; -use ratatui::style::Color; -use ratatui::style::Modifier; -use ratatui::style::Style; -use ratatui::text::Line; -use ratatui::text::Span; -use ratatui::widgets::Block; -use ratatui::widgets::BorderType; -use ratatui::widgets::Borders; -use ratatui::widgets::Cell; -use ratatui::widgets::Row; -use ratatui::widgets::Table; -use ratatui::widgets::Widget; use ratatui::widgets::WidgetRef; -/// Maximum number of suggestions shown in the popup. -const MAX_RESULTS: usize = 8; +use super::popup_consts::MAX_POPUP_ROWS; +use super::scroll_state::ScrollState; +use super::selection_popup_common::GenericDisplayRow; +use super::selection_popup_common::render_rows; /// Visual state for the file-search popup. pub(crate) struct FileSearchPopup { @@ -30,8 +19,8 @@ pub(crate) struct FileSearchPopup { waiting: bool, /// Cached matches; paths relative to the search dir. matches: Vec, - /// Currently selected index inside `matches` (if any). - selected_idx: Option, + /// Shared selection/scroll state. + state: ScrollState, } impl FileSearchPopup { @@ -41,7 +30,7 @@ impl FileSearchPopup { pending_query: String::new(), waiting: true, matches: Vec::new(), - selected_idx: None, + state: ScrollState::new(), } } @@ -61,7 +50,7 @@ impl FileSearchPopup { if !keep_existing { self.matches.clear(); - self.selected_idx = None; + self.state.reset(); } } @@ -75,40 +64,32 @@ impl FileSearchPopup { self.display_query = query.to_string(); self.matches = matches; self.waiting = false; - self.selected_idx = if self.matches.is_empty() { - None - } else { - Some(0) - }; + let len = self.matches.len(); + self.state.clamp_selection(len); + self.state.ensure_visible(len, len.min(MAX_POPUP_ROWS)); } /// Move selection cursor up. pub(crate) fn move_up(&mut self) { - if let Some(idx) = self.selected_idx { - if idx > 0 { - self.selected_idx = Some(idx - 1); - } - } + let len = self.matches.len(); + self.state.move_up_wrap(len); + self.state.ensure_visible(len, len.min(MAX_POPUP_ROWS)); } /// Move selection cursor down. pub(crate) fn move_down(&mut self) { - if let Some(idx) = self.selected_idx { - if idx + 1 < self.matches.len() { - self.selected_idx = Some(idx + 1); - } - } else if !self.matches.is_empty() { - self.selected_idx = Some(0); - } + let len = self.matches.len(); + self.state.move_down_wrap(len); + self.state.ensure_visible(len, len.min(MAX_POPUP_ROWS)); } pub(crate) fn selected_match(&self) -> Option<&str> { - self.selected_idx + self.state + .selected_idx .and_then(|idx| self.matches.get(idx)) .map(|file_match| file_match.path.as_str()) } - /// Preferred height (rows) including border. pub(crate) fn calculate_required_height(&self) -> u16 { // Row count depends on whether we already have matches. If no matches // yet (e.g. initial search or query with no results) reserve a single @@ -116,71 +97,35 @@ impl FileSearchPopup { // up to MAX_RESULTS regardless of the waiting flag so the list // remains stable while a newer search is in-flight. - self.matches.len().clamp(1, MAX_RESULTS) as u16 + self.matches.len().clamp(1, MAX_POPUP_ROWS) as u16 } } impl WidgetRef for &FileSearchPopup { fn render_ref(&self, area: Rect, buf: &mut Buffer) { - // Prepare rows. - let rows: Vec = if self.matches.is_empty() { - vec![Row::new(vec![ - Cell::from(if self.waiting { - "(searching …)" - } else { - "no matches" - }) - .style(Style::new().add_modifier(Modifier::ITALIC | Modifier::DIM)), - ])] + // Convert matches to GenericDisplayRow, translating indices to usize at the UI boundary. + let rows_all: Vec = if self.matches.is_empty() { + Vec::new() } else { self.matches .iter() - .take(MAX_RESULTS) - .enumerate() - .map(|(i, file_match)| { - let FileMatch { path, indices, .. } = file_match; - let path = path.as_str(); - #[allow(clippy::expect_used)] - let indices = indices.as_ref().expect("indices should be present"); - - // Build spans with bold on matching indices. - let mut idx_iter = indices.iter().peekable(); - let mut spans: Vec = Vec::with_capacity(path.len()); - - for (char_idx, ch) in path.chars().enumerate() { - let mut style = Style::default(); - if idx_iter - .peek() - .is_some_and(|next| **next == char_idx as u32) - { - idx_iter.next(); - style = style.add_modifier(Modifier::BOLD); - } - spans.push(Span::styled(ch.to_string(), style)); - } - - // Create cell from the spans. - let mut cell = Cell::from(Line::from(spans)); - - // If selected, also paint yellow. - if Some(i) == self.selected_idx { - cell = cell.style(Style::default().fg(Color::Yellow)); - } - - Row::new(vec![cell]) + .map(|m| GenericDisplayRow { + name: m.path.clone(), + match_indices: m + .indices + .as_ref() + .map(|v| v.iter().map(|&i| i as usize).collect()), + is_current: false, + description: None, }) .collect() }; - let table = Table::new(rows, vec![Constraint::Percentage(100)]) - .block( - Block::default() - .borders(Borders::LEFT) - .border_type(BorderType::QuadrantOutside) - .border_style(Style::default().fg(Color::DarkGray)), - ) - .widths([Constraint::Percentage(100)]); - - table.render(area, buf); + if self.waiting && rows_all.is_empty() { + // Render a minimal waiting stub using the shared renderer (no rows -> "no matches"). + render_rows(area, buf, &[], &self.state, MAX_POPUP_ROWS); + } else { + render_rows(area, buf, &rows_all, &self.state, MAX_POPUP_ROWS); + } } } diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index cdb01ba06a..ff3cf2f2c4 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -19,6 +19,9 @@ mod chat_composer_history; mod command_popup; mod file_search_popup; mod live_ring_widget; +mod popup_consts; +mod scroll_state; +mod selection_popup_common; mod status_indicator_view; mod textarea; diff --git a/codex-rs/tui/src/bottom_pane/popup_consts.rs b/codex-rs/tui/src/bottom_pane/popup_consts.rs new file mode 100644 index 0000000000..5f447d735c --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/popup_consts.rs @@ -0,0 +1,5 @@ +//! Shared popup-related constants for bottom pane widgets. + +/// Maximum number of rows any popup should attempt to display. +/// Keep this consistent across all popups for a uniform feel. +pub(crate) const MAX_POPUP_ROWS: usize = 8; diff --git a/codex-rs/tui/src/bottom_pane/scroll_state.rs b/codex-rs/tui/src/bottom_pane/scroll_state.rs new file mode 100644 index 0000000000..a9728d1a0d --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/scroll_state.rs @@ -0,0 +1,115 @@ +/// Generic scroll/selection state for a vertical list menu. +/// +/// Encapsulates the common behavior of a selectable list that supports: +/// - Optional selection (None when list is empty) +/// - Wrap-around navigation on Up/Down +/// - Maintaining a scroll window (`scroll_top`) so the selected row stays visible +#[derive(Debug, Default, Clone, Copy)] +pub(crate) struct ScrollState { + pub selected_idx: Option, + pub scroll_top: usize, +} + +impl ScrollState { + pub fn new() -> Self { + Self { + selected_idx: None, + scroll_top: 0, + } + } + + /// Reset selection and scroll. + pub fn reset(&mut self) { + self.selected_idx = None; + self.scroll_top = 0; + } + + /// Clamp selection to be within the [0, len-1] range, or None when empty. + pub fn clamp_selection(&mut self, len: usize) { + self.selected_idx = match len { + 0 => None, + _ => Some(self.selected_idx.unwrap_or(0).min(len - 1)), + }; + if len == 0 { + self.scroll_top = 0; + } + } + + /// Move selection up by one, wrapping to the bottom when necessary. + pub fn move_up_wrap(&mut self, len: usize) { + if len == 0 { + self.selected_idx = None; + self.scroll_top = 0; + return; + } + self.selected_idx = Some(match self.selected_idx { + Some(idx) if idx > 0 => idx - 1, + Some(_) => len - 1, + None => 0, + }); + } + + /// Move selection down by one, wrapping to the top when necessary. + pub fn move_down_wrap(&mut self, len: usize) { + if len == 0 { + self.selected_idx = None; + self.scroll_top = 0; + return; + } + self.selected_idx = Some(match self.selected_idx { + Some(idx) if idx + 1 < len => idx + 1, + _ => 0, + }); + } + + /// Adjust `scroll_top` so that the current `selected_idx` is visible within + /// the window of `visible_rows`. + pub fn ensure_visible(&mut self, len: usize, visible_rows: usize) { + if len == 0 || visible_rows == 0 { + self.scroll_top = 0; + return; + } + if let Some(sel) = self.selected_idx { + if sel < self.scroll_top { + self.scroll_top = sel; + } else { + let bottom = self.scroll_top + visible_rows - 1; + if sel > bottom { + self.scroll_top = sel + 1 - visible_rows; + } + } + } else { + self.scroll_top = 0; + } + } +} + +#[cfg(test)] +mod tests { + use super::ScrollState; + + #[test] + fn wrap_navigation_and_visibility() { + let mut s = ScrollState::new(); + let len = 10; + let vis = 5; + + s.clamp_selection(len); + assert_eq!(s.selected_idx, Some(0)); + s.ensure_visible(len, vis); + assert_eq!(s.scroll_top, 0); + + s.move_up_wrap(len); + s.ensure_visible(len, vis); + assert_eq!(s.selected_idx, Some(len - 1)); + match s.selected_idx { + Some(sel) => assert!(s.scroll_top <= sel), + None => panic!("expected Some(selected_idx) after wrap"), + } + + s.move_down_wrap(len); + s.ensure_visible(len, vis); + assert_eq!(s.selected_idx, Some(0)); + assert_eq!(s.scroll_top, 0); + } +} diff --git a/codex-rs/tui/src/bottom_pane/selection_popup_common.rs b/codex-rs/tui/src/bottom_pane/selection_popup_common.rs new file mode 100644 index 0000000000..1a31115d77 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/selection_popup_common.rs @@ -0,0 +1,126 @@ +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::prelude::Constraint; +use ratatui::style::Color; +use ratatui::style::Modifier; +use ratatui::style::Style; +use ratatui::text::Line; +use ratatui::text::Span; +use ratatui::widgets::Block; +use ratatui::widgets::BorderType; +use ratatui::widgets::Borders; +use ratatui::widgets::Cell; +use ratatui::widgets::Row; +use ratatui::widgets::Table; +use ratatui::widgets::Widget; + +use super::scroll_state::ScrollState; + +/// A generic representation of a display row for selection popups. +pub(crate) struct GenericDisplayRow { + pub name: String, + pub match_indices: Option>, // indices to bold (char positions) + pub is_current: bool, + pub description: Option, // optional grey text after the name +} + +impl GenericDisplayRow {} + +/// Render a list of rows using the provided ScrollState, with shared styling +/// and behavior for selection popups. +pub(crate) fn render_rows( + area: Rect, + buf: &mut Buffer, + rows_all: &[GenericDisplayRow], + state: &ScrollState, + max_results: usize, +) { + let mut rows: Vec = Vec::new(); + if rows_all.is_empty() { + rows.push(Row::new(vec![Cell::from(Line::from(Span::styled( + "no matches", + Style::default().add_modifier(Modifier::ITALIC | Modifier::DIM), + )))])); + } else { + let max_rows_from_area = area.height as usize; + let visible_rows = max_results + .min(rows_all.len()) + .min(max_rows_from_area.max(1)); + + // Compute starting index based on scroll state and selection. + let mut start_idx = state.scroll_top.min(rows_all.len().saturating_sub(1)); + if let Some(sel) = state.selected_idx { + if sel < start_idx { + start_idx = sel; + } else if visible_rows > 0 { + let bottom = start_idx + visible_rows - 1; + if sel > bottom { + start_idx = sel + 1 - visible_rows; + } + } + } + + for (i, row) in rows_all + .iter() + .enumerate() + .skip(start_idx) + .take(visible_rows) + { + let GenericDisplayRow { + name, + match_indices, + is_current, + description, + } = row; + + // Highlight fuzzy indices when present. + let mut spans: Vec = Vec::with_capacity(name.len()); + if let Some(idxs) = match_indices.as_ref() { + let mut idx_iter = idxs.iter().peekable(); + for (char_idx, ch) in name.chars().enumerate() { + let mut style = Style::default(); + if idx_iter.peek().is_some_and(|next| **next == char_idx) { + idx_iter.next(); + style = style.add_modifier(Modifier::BOLD); + } + spans.push(Span::styled(ch.to_string(), style)); + } + } else { + spans.push(Span::raw(name.clone())); + } + + if let Some(desc) = description.as_ref() { + spans.push(Span::raw(" ")); + spans.push(Span::styled( + desc.clone(), + Style::default() + .fg(Color::DarkGray) + .add_modifier(Modifier::DIM), + )); + } + + let mut cell = Cell::from(Line::from(spans)); + if Some(i) == state.selected_idx { + cell = cell.style( + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ); + } else if *is_current { + cell = cell.style(Style::default().fg(Color::Cyan)); + } + rows.push(Row::new(vec![cell])); + } + } + + let table = Table::new(rows, vec![Constraint::Percentage(100)]) + .block( + Block::default() + .borders(Borders::LEFT) + .border_type(BorderType::QuadrantOutside) + .border_style(Style::default().fg(Color::DarkGray)), + ) + .widths([Constraint::Percentage(100)]); + + table.render(area, buf); +} From ec20e84d80c5216234b39c4148febef86126fd8e Mon Sep 17 00:00:00 2001 From: aibrahim-oai Date: Wed, 6 Aug 2025 22:25:41 -0700 Subject: [PATCH 0060/1309] Change the UI of apply patch (#1907) image --------- Co-authored-by: Gabriel Peal --- codex-rs/Cargo.lock | 35 +++-- codex-rs/tui/Cargo.toml | 4 +- codex-rs/tui/src/chatwidget.rs | 8 +- codex-rs/tui/src/history_cell.rs | 212 ++++++++++++++++++++----------- 4 files changed, 168 insertions(+), 91 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 4e21baf7f5..aa5398dde1 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -873,6 +873,7 @@ dependencies = [ "codex-ollama", "color-eyre", "crossterm", + "diffy", "image", "insta", "lazy_static", @@ -1255,6 +1256,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "diffy" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b545b8c50194bdd008283985ab0b31dba153cfd5b3066a92770634fbc0d7d291" +dependencies = [ + "nu-ansi-term 0.50.1", +] + [[package]] name = "digest" version = "0.10.7" @@ -1493,7 +1503,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -1573,7 +1583,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", "rustix 1.0.8", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2356,7 +2366,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2832,6 +2842,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "nucleo-matcher" version = "0.3.1" @@ -3740,7 +3759,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3753,7 +3772,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -4519,7 +4538,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix 1.0.8", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4960,7 +4979,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", - "nu-ansi-term", + "nu-ansi-term 0.46.0", "once_cell", "regex", "sharded-slab", @@ -5378,7 +5397,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 49d843f046..719c631149 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -36,6 +36,7 @@ codex-login = { path = "../login" } codex-ollama = { path = "../ollama" } color-eyre = "0.6.3" crossterm = { version = "0.28.1", features = ["bracketed-paste"] } +diffy = "0.4.2" image = { version = "^0.25.6", default-features = false, features = ["jpeg"] } lazy_static = "1" mcp-types = { path = "../mcp-types" } @@ -72,10 +73,9 @@ unicode-width = "0.1" uuid = "1" - [dev-dependencies] +chrono = { version = "0.4", features = ["serde"] } insta = "1.43.1" pretty_assertions = "1" rand = "0.8" -chrono = { version = "0.4", features = ["serde"] } vt100 = "0.16.2" diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 50fa776ec3..9936c0eef9 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -476,11 +476,9 @@ impl ChatWidget<'_> { )); } EventMsg::PatchApplyEnd(event) => { - self.add_to_history(HistoryCell::new_patch_apply_end( - event.stdout, - event.stderr, - event.success, - )); + if !event.success { + self.add_to_history(HistoryCell::new_patch_apply_failure(event.stderr)); + } } EventMsg::ExecCommandEnd(ExecCommandEndEvent { call_id, diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 5caedf98ab..79a17f4a63 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -40,6 +40,12 @@ pub(crate) struct CommandOutput { pub(crate) stderr: String, } +struct FileSummary { + display_path: String, + added: usize, + removed: usize, +} + pub(crate) enum PatchEventType { ApprovalRequest, ApplyBegin { auto_approved: bool }, @@ -599,12 +605,12 @@ impl HistoryCell { PatchEventType::ApprovalRequest => "proposed patch", PatchEventType::ApplyBegin { auto_approved: true, - } => "applying patch", + } => "✏️ Applying patch", PatchEventType::ApplyBegin { auto_approved: false, } => { let lines: Vec> = vec![ - Line::from("applying patch".magenta().bold()), + Line::from("✏️ Applying patch".magenta().bold()), Line::from(""), ]; return Self::PendingPatch { @@ -613,39 +619,12 @@ impl HistoryCell { } }; - let summary_lines = create_diff_summary(changes); + let summary_lines = create_diff_summary(title, changes); let mut lines: Vec> = Vec::new(); - // Header similar to the command formatter so patches are visually - // distinct while still fitting the overall colour scheme. - lines.push(Line::from(title.magenta().bold())); - for line in summary_lines { - if line.starts_with('+') { - lines.push(line.green().into()); - } else if line.starts_with('-') { - lines.push(line.red().into()); - } else if let Some(space_idx) = line.find(' ') { - let kind_owned = line[..space_idx].to_string(); - let rest_owned = line[space_idx + 1..].to_string(); - - let style_for = |fg: Color| Style::default().fg(fg).add_modifier(Modifier::BOLD); - - let styled_kind = match kind_owned.as_str() { - "A" => RtSpan::styled(kind_owned.clone(), style_for(Color::Green)), - "D" => RtSpan::styled(kind_owned.clone(), style_for(Color::Red)), - "M" => RtSpan::styled(kind_owned.clone(), style_for(Color::Yellow)), - "R" | "C" => RtSpan::styled(kind_owned.clone(), style_for(Color::Cyan)), - _ => RtSpan::raw(kind_owned.clone()), - }; - - let styled_line = - RtLine::from(vec![styled_kind, RtSpan::raw(" "), RtSpan::raw(rest_owned)]); - lines.push(styled_line); - } else { - lines.push(Line::from(line)); - } + lines.push(line); } lines.push(Line::from("")); @@ -655,44 +634,23 @@ impl HistoryCell { } } - pub(crate) fn new_patch_apply_end(stdout: String, stderr: String, success: bool) -> Self { + pub(crate) fn new_patch_apply_failure(stderr: String) -> Self { let mut lines: Vec> = Vec::new(); - let status = if success { - RtSpan::styled("patch applied", Style::default().fg(Color::Green)) - } else { - RtSpan::styled( - "patch failed", - Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), - ) - }; - lines.push(RtLine::from(vec![ - "patch".magenta().bold(), - " ".into(), - status, - ])); + // Failure title + lines.push(Line::from("✘ Failed to apply patch".magenta().bold())); - let src = if success { - if stdout.trim().is_empty() { - &stderr - } else { - &stdout - } - } else if stderr.trim().is_empty() { - &stdout - } else { - &stderr - }; - - if !src.trim().is_empty() { - lines.push(Line::from("")); - let mut iter = src.lines(); - for raw in iter.by_ref().take(TOOL_CALL_MAX_LINES) { - lines.push(ansi_escape_line(raw).dim()); + if !stderr.trim().is_empty() { + let mut iter = stderr.lines(); + for (i, raw) in iter.by_ref().take(TOOL_CALL_MAX_LINES).enumerate() { + let prefix = if i == 0 { " ⎿ " } else { " " }; + let s = format!("{prefix}{raw}"); + lines.push(ansi_escape_line(&s).dim()); } let remaining = iter.count(); if remaining > 0 { - lines.push(Line::from(format!("... {remaining} additional lines")).dim()); + lines.push(Line::from("")); + lines.push(Line::from(format!("... +{remaining} lines")).dim()); } } @@ -712,36 +670,138 @@ impl WidgetRef for &HistoryCell { } } -fn create_diff_summary(changes: HashMap) -> Vec { - // Build a concise, human‑readable summary list similar to the - // `git status` short format so the user can reason about the - // patch without scrolling. - let mut summaries: Vec = Vec::new(); +fn create_diff_summary(title: &str, changes: HashMap) -> Vec> { + let mut files: Vec = Vec::new(); + + // Count additions/deletions from a unified diff body + let count_from_unified = |diff: &str| -> (usize, usize) { + if let Ok(patch) = diffy::Patch::from_str(diff) { + let mut adds = 0usize; + let mut dels = 0usize; + for hunk in patch.hunks() { + for line in hunk.lines() { + match line { + diffy::Line::Insert(_) => adds += 1, + diffy::Line::Delete(_) => dels += 1, + _ => {} + } + } + } + (adds, dels) + } else { + let mut adds = 0usize; + let mut dels = 0usize; + for l in diff.lines() { + if l.starts_with("+++") || l.starts_with("---") || l.starts_with("@@") { + continue; + } + match l.as_bytes().first() { + Some(b'+') => adds += 1, + Some(b'-') => dels += 1, + _ => {} + } + } + (adds, dels) + } + }; + for (path, change) in &changes { use codex_core::protocol::FileChange::*; match change { Add { content } => { let added = content.lines().count(); - summaries.push(format!("A {} (+{added})", path.display())); + files.push(FileSummary { + display_path: path.display().to_string(), + added, + removed: 0, + }); } Delete => { - summaries.push(format!("D {}", path.display())); + let removed = std::fs::read_to_string(path) + .ok() + .map(|s| s.lines().count()) + .unwrap_or(0); + files.push(FileSummary { + display_path: path.display().to_string(), + added: 0, + removed, + }); } Update { unified_diff, move_path, } => { - if let Some(new_path) = move_path { - summaries.push(format!("R {} → {}", path.display(), new_path.display(),)); + let (added, removed) = count_from_unified(unified_diff); + let display_path = if let Some(new_path) = move_path { + format!("{} → {}", path.display(), new_path.display()) } else { - summaries.push(format!("M {}", path.display(),)); - } - summaries.extend(unified_diff.lines().map(|s| s.to_string())); + path.display().to_string() + }; + files.push(FileSummary { + display_path, + added, + removed, + }); } } } - summaries + let file_count = files.len(); + let total_added: usize = files.iter().map(|f| f.added).sum(); + let total_removed: usize = files.iter().map(|f| f.removed).sum(); + let noun = if file_count == 1 { "file" } else { "files" }; + + let mut out: Vec> = Vec::new(); + + // Header + let mut header_spans: Vec> = Vec::new(); + header_spans.push(RtSpan::styled( + title.to_owned(), + Style::default() + .fg(Color::Magenta) + .add_modifier(Modifier::BOLD), + )); + header_spans.push(RtSpan::raw(" to ")); + header_spans.push(RtSpan::raw(format!("{file_count} {noun} "))); + header_spans.push(RtSpan::raw("(")); + header_spans.push(RtSpan::styled( + format!("+{total_added}"), + Style::default().fg(Color::Green), + )); + header_spans.push(RtSpan::raw(" ")); + header_spans.push(RtSpan::styled( + format!("-{total_removed}"), + Style::default().fg(Color::Red), + )); + header_spans.push(RtSpan::raw(")")); + out.push(RtLine::from(header_spans)); + + // Dimmed per-file lines with prefix + for (idx, f) in files.iter().enumerate() { + let mut spans: Vec> = Vec::new(); + spans.push(RtSpan::raw(f.display_path.clone())); + spans.push(RtSpan::raw(" (")); + spans.push(RtSpan::styled( + format!("+{}", f.added), + Style::default().fg(Color::Green), + )); + spans.push(RtSpan::raw(" ")); + spans.push(RtSpan::styled( + format!("-{}", f.removed), + Style::default().fg(Color::Red), + )); + spans.push(RtSpan::raw(")")); + + let mut line = RtLine::from(spans); + let prefix = if idx == 0 { " ⎿ " } else { " " }; + line.spans.insert(0, prefix.into()); + line.spans.iter_mut().for_each(|span| { + span.style = span.style.add_modifier(Modifier::DIM); + }); + out.push(line); + } + + out } fn format_mcp_invocation<'a>(invocation: McpInvocation) -> Line<'a> { From 935ad5c6f2f6dce653886cb2ec747d1bba074ea9 Mon Sep 17 00:00:00 2001 From: ae Date: Wed, 6 Aug 2025 22:54:54 -0700 Subject: [PATCH 0061/1309] feat: >_ (#1924) --- codex-rs/exec/src/cli.rs | 1 + codex-rs/tui/src/cli.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/codex-rs/exec/src/cli.rs b/codex-rs/exec/src/cli.rs index ea659e3252..b6c48b9dc6 100644 --- a/codex-rs/exec/src/cli.rs +++ b/codex-rs/exec/src/cli.rs @@ -34,6 +34,7 @@ pub struct Cli { /// EXTREMELY DANGEROUS. Intended solely for running in environments that are externally sandboxed. #[arg( long = "dangerously-bypass-approvals-and-sandbox", + alias = "yolo", default_value_t = false, conflicts_with = "full_auto" )] diff --git a/codex-rs/tui/src/cli.rs b/codex-rs/tui/src/cli.rs index 85dffbebb3..078936dc33 100644 --- a/codex-rs/tui/src/cli.rs +++ b/codex-rs/tui/src/cli.rs @@ -44,6 +44,7 @@ pub struct Cli { /// EXTREMELY DANGEROUS. Intended solely for running in environments that are externally sandboxed. #[arg( long = "dangerously-bypass-approvals-and-sandbox", + alias = "yolo", default_value_t = false, conflicts_with_all = ["approval_policy", "full_auto"] )] From f0fe61c66772ae88e9892e62cb98f684af2d8917 Mon Sep 17 00:00:00 2001 From: ae Date: Wed, 6 Aug 2025 23:22:58 -0700 Subject: [PATCH 0062/1309] feat: use ctrl c in interrupt hint (#1926) https://chatgpt.com/codex/tasks/task_i_689441c33e1c832c85ceda166dab5d33 --- codex-rs/tui/src/status_indicator_widget.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/codex-rs/tui/src/status_indicator_widget.rs b/codex-rs/tui/src/status_indicator_widget.rs index 8cca0fb04d..fca9a23bc9 100644 --- a/codex-rs/tui/src/status_indicator_widget.rs +++ b/codex-rs/tui/src/status_indicator_widget.rs @@ -213,21 +213,21 @@ impl WidgetRef for StatusIndicatorWidget { // Plain rendering: no borders or padding so the live cell is visually indistinguishable from terminal scrollback. let inner_width = area.width as usize; - // Compose a single status line like: "▌ Working (Xs • Ctrl z to interrupt) " + // Compose a single status line like: "▌ Working (Xs • Ctrl c to interrupt) " let mut spans: Vec> = Vec::new(); spans.push(Span::styled("▌ ", Style::default().fg(Color::Cyan))); // Animated header after the left bar spans.extend(animated_spans); // Space between header and bracket block spans.push(Span::raw(" ")); - // Non-animated, dim bracket content, with only "Ctrl z" bold + // Non-animated, dim bracket content, with only "Ctrl c" bold let bracket_prefix = format!("({elapsed}s • "); spans.push(Span::styled( bracket_prefix, Style::default().fg(Color::Gray).add_modifier(Modifier::DIM), )); spans.push(Span::styled( - "Ctrl z", + "Ctrl c", Style::default() .fg(Color::Gray) .add_modifier(Modifier::DIM | Modifier::BOLD), From f15e0fe1dfc710c7f0185ab4e0f441ee336c0985 Mon Sep 17 00:00:00 2001 From: aibrahim-oai Date: Wed, 6 Aug 2025 23:25:56 -0700 Subject: [PATCH 0063/1309] Ensure exec command end always emitted (#1908) ## Summary - defer ExecCommandEnd emission until after sandbox resolution - make sandbox error handler return final exec output and response - align sandbox error stderr with response content and rename to `final_output` - replace unstable `let` chains in client command header logic ## Testing - `just fmt` - `just fix` - `cargo test --all-features` *(fails: NotPresent in core/tests/client.rs)* ------ https://chatgpt.com/codex/tasks/task_i_6893e63b0c408321a8e1ff2a052c4c51 --- codex-rs/core/src/codex.rs | 186 ++++++++++++++++++++++--------------- codex-rs/core/src/error.rs | 7 ++ 2 files changed, 120 insertions(+), 73 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 98d13b4cd6..4a4faa84ee 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -46,6 +46,7 @@ use crate::conversation_history::ConversationHistory; use crate::error::CodexErr; use crate::error::Result as CodexResult; use crate::error::SandboxErr; +use crate::error::get_error_message_ui; use crate::exec::ExecParams; use crate::exec::ExecToolCallOutput; use crate::exec::SandboxType; @@ -468,6 +469,57 @@ impl Session { } } } + /// Runs the exec tool call and emits events for the begin and end of the + /// command even on error. + /// + /// Returns the output of the exec tool call. + async fn run_exec_with_events<'a>( + &self, + turn_diff_tracker: &mut TurnDiffTracker, + begin_ctx: ExecCommandContext, + exec_args: ExecInvokeArgs<'a>, + ) -> crate::error::Result { + let is_apply_patch = begin_ctx.apply_patch.is_some(); + let sub_id = begin_ctx.sub_id.clone(); + let call_id = begin_ctx.call_id.clone(); + + self.on_exec_command_begin(turn_diff_tracker, begin_ctx.clone()) + .await; + + let result = process_exec_tool_call( + exec_args.params, + exec_args.sandbox_type, + exec_args.ctrl_c, + exec_args.sandbox_policy, + exec_args.codex_linux_sandbox_exe, + exec_args.stdout_stream, + ) + .await; + + let output_stderr; + let borrowed: &ExecToolCallOutput = match &result { + Ok(output) => output, + Err(e) => { + output_stderr = ExecToolCallOutput { + exit_code: -1, + stdout: String::new(), + stderr: get_error_message_ui(e), + duration: Duration::default(), + }; + &output_stderr + } + }; + self.on_exec_command_end( + turn_diff_tracker, + &sub_id, + &call_id, + borrowed, + is_apply_patch, + ) + .await; + + result + } /// Helper that emits a BackgroundEvent with the given message. This keeps /// the call‑sites terse so adding more diagnostics does not clutter the @@ -1717,6 +1769,15 @@ fn parse_container_exec_arguments( } } +pub struct ExecInvokeArgs<'a> { + pub params: ExecParams, + pub sandbox_type: SandboxType, + pub ctrl_c: Arc, + pub sandbox_policy: &'a SandboxPolicy, + pub codex_linux_sandbox_exe: &'a Option, + pub stdout_stream: Option, +} + fn maybe_run_with_user_profile(params: ExecParams, sess: &Session) -> ExecParams { if sess.shell_environment_policy.use_profile { let command = sess @@ -1887,23 +1948,26 @@ async fn handle_container_exec_with_params( }, ), }; - sess.on_exec_command_begin(turn_diff_tracker, exec_command_context.clone()) - .await; let params = maybe_run_with_user_profile(params, sess); - let output_result = process_exec_tool_call( - params.clone(), - sandbox_type, - sess.ctrl_c.clone(), - &sess.sandbox_policy, - &sess.codex_linux_sandbox_exe, - Some(StdoutStream { - sub_id: sub_id.clone(), - call_id: call_id.clone(), - tx_event: sess.tx_event.clone(), - }), - ) - .await; + let output_result = sess + .run_exec_with_events( + turn_diff_tracker, + exec_command_context.clone(), + ExecInvokeArgs { + params: params.clone(), + sandbox_type, + ctrl_c: sess.ctrl_c.clone(), + sandbox_policy: &sess.sandbox_policy, + codex_linux_sandbox_exe: &sess.codex_linux_sandbox_exe, + stdout_stream: Some(StdoutStream { + sub_id: sub_id.clone(), + call_id: call_id.clone(), + tx_event: sess.tx_event.clone(), + }), + }, + ) + .await; match output_result { Ok(output) => { @@ -1914,24 +1978,14 @@ async fn handle_container_exec_with_params( duration, } = &output; - sess.on_exec_command_end( - turn_diff_tracker, - &sub_id, - &call_id, - &output, - exec_command_context.apply_patch.is_some(), - ) - .await; - let is_success = *exit_code == 0; let content = format_exec_output( if is_success { stdout } else { stderr }, *exit_code, *duration, ); - ResponseInputItem::FunctionCallOutput { - call_id, + call_id: call_id.clone(), output: FunctionCallOutputPayload { content, success: Some(is_success), @@ -1949,16 +2003,13 @@ async fn handle_container_exec_with_params( ) .await } - Err(e) => { - // Handle non-sandbox errors - ResponseInputItem::FunctionCallOutput { - call_id, - output: FunctionCallOutputPayload { - content: format!("execution error: {e}"), - success: None, - }, - } - } + Err(e) => ResponseInputItem::FunctionCallOutput { + call_id: call_id.clone(), + output: FunctionCallOutputPayload { + content: format!("execution error: {e}"), + success: None, + }, + }, } } @@ -1973,7 +2024,6 @@ async fn handle_sandbox_error( let call_id = exec_command_context.call_id.clone(); let sub_id = exec_command_context.sub_id.clone(); let cwd = exec_command_context.cwd.clone(); - let is_apply_patch = exec_command_context.apply_patch.is_some(); // Early out if either the user never wants to be asked for approval, or // we're letting the model manage escalation requests. Otherwise, continue @@ -2039,24 +2089,26 @@ async fn handle_sandbox_error( sess.notify_background_event(&sub_id, "retrying command without sandbox") .await; - sess.on_exec_command_begin(turn_diff_tracker, exec_command_context) - .await; - // This is an escalated retry; the policy will not be // examined and the sandbox has been set to `None`. - let retry_output_result = process_exec_tool_call( - params, - SandboxType::None, - sess.ctrl_c.clone(), - &sess.sandbox_policy, - &sess.codex_linux_sandbox_exe, - Some(StdoutStream { - sub_id: sub_id.clone(), - call_id: call_id.clone(), - tx_event: sess.tx_event.clone(), - }), - ) - .await; + let retry_output_result = sess + .run_exec_with_events( + turn_diff_tracker, + exec_command_context.clone(), + ExecInvokeArgs { + params, + sandbox_type: SandboxType::None, + ctrl_c: sess.ctrl_c.clone(), + sandbox_policy: &sess.sandbox_policy, + codex_linux_sandbox_exe: &sess.codex_linux_sandbox_exe, + stdout_stream: Some(StdoutStream { + sub_id: sub_id.clone(), + call_id: call_id.clone(), + tx_event: sess.tx_event.clone(), + }), + }, + ) + .await; match retry_output_result { Ok(retry_output) => { @@ -2067,15 +2119,6 @@ async fn handle_sandbox_error( duration, } = &retry_output; - sess.on_exec_command_end( - turn_diff_tracker, - &sub_id, - &call_id, - &retry_output, - is_apply_patch, - ) - .await; - let is_success = *exit_code == 0; let content = format_exec_output( if is_success { stdout } else { stderr }, @@ -2084,23 +2127,20 @@ async fn handle_sandbox_error( ); ResponseInputItem::FunctionCallOutput { - call_id, + call_id: call_id.clone(), output: FunctionCallOutputPayload { content, success: Some(is_success), }, } } - Err(e) => { - // Handle retry failure - ResponseInputItem::FunctionCallOutput { - call_id, - output: FunctionCallOutputPayload { - content: format!("retry failed: {e}"), - success: None, - }, - } - } + Err(e) => ResponseInputItem::FunctionCallOutput { + call_id: call_id.clone(), + output: FunctionCallOutputPayload { + content: format!("retry failed: {e}"), + success: None, + }, + }, } } ReviewDecision::Denied | ReviewDecision::Abort => { diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index 9cdc4eb544..537f4a0361 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -132,3 +132,10 @@ impl CodexErr { (self as &dyn std::any::Any).downcast_ref::() } } + +pub fn get_error_message_ui(e: &CodexErr) -> String { + match e { + CodexErr::Sandbox(SandboxErr::Denied(_, _, stderr)) => stderr.to_string(), + _ => e.to_string(), + } +} From fff2bb39f9277850c70b0f00b92be30640a52b0a Mon Sep 17 00:00:00 2001 From: aibrahim-oai Date: Thu, 7 Aug 2025 00:01:38 -0700 Subject: [PATCH 0064/1309] change todo (#1925) image image --- codex-rs/tui/src/history_cell.rs | 116 +++++++++++++++++++------------ 1 file changed, 71 insertions(+), 45 deletions(-) diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 79a17f4a63..be96a9e9ef 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -513,48 +513,48 @@ impl HistoryCell { } } - /// Render a user‑friendly plan update with colourful status icons and a - /// simple progress indicator so users can follow along. + /// Render a user‑friendly plan update styled like a checkbox todo list. pub(crate) fn new_plan_update(update: UpdatePlanArgs) -> Self { let UpdatePlanArgs { explanation, plan } = update; let mut lines: Vec> = Vec::new(); + // Header with progress summary + let total = plan.len(); + let completed = plan + .iter() + .filter(|p| matches!(p.status, StepStatus::Completed)) + .count(); - // Title - lines.push(Line::from("plan".magenta().bold())); + let width: usize = 10; + let filled = if total > 0 { + (completed * width + total / 2) / total + } else { + 0 + }; + let empty = width.saturating_sub(filled); - if !plan.is_empty() { - // Progress bar – show completed/total with a visual bar - let total = plan.len(); - let completed = plan - .iter() - .filter(|p| matches!(p.status, StepStatus::Completed)) - .count(); - let width: usize = 20; - let filled = (completed * width + total / 2) / total; - let empty = width.saturating_sub(filled); - let mut bar_spans: Vec = Vec::new(); - if filled > 0 { - bar_spans.push(Span::styled( - "█".repeat(filled), - Style::default().fg(Color::Green), - )); - } - if empty > 0 { - bar_spans.push(Span::styled( - "░".repeat(empty), - Style::default().fg(Color::Gray), - )); - } - let progress_prefix = Span::raw("progress ["); - let progress_suffix = Span::raw("] "); - let fraction = Span::raw(format!("{completed}/{total}")); - let mut progress_line_spans = vec![progress_prefix]; - progress_line_spans.extend(bar_spans); - progress_line_spans.push(progress_suffix); - progress_line_spans.push(fraction); - lines.push(Line::from(progress_line_spans)); + let mut header: Vec = Vec::new(); + header.push(Span::raw("📋")); + header.push(Span::styled( + "Updated", + Style::default().add_modifier(Modifier::BOLD).magenta(), + )); + header.push(Span::raw(" to do list [")); + if filled > 0 { + header.push(Span::styled( + "█".repeat(filled), + Style::default().fg(Color::Green), + )); } + if empty > 0 { + header.push(Span::styled( + "░".repeat(empty), + Style::default().fg(Color::Gray), + )); + } + header.push(Span::raw("] ")); + header.push(Span::raw(format!("{completed}/{total}"))); + lines.push(Line::from(header)); // Optional explanation/note from the model if let Some(expl) = explanation.and_then(|s| { @@ -567,22 +567,48 @@ impl HistoryCell { } } - // Steps (1‑based numbering) with fun, readable status icons + // Steps styled as checkbox items if plan.is_empty() { lines.push(Line::from("(no steps provided)".gray().italic())); } else { for (idx, PlanItemArg { step, status }) in plan.into_iter().enumerate() { - let num = idx + 1; - let icon_span: Span = match status { - StepStatus::Completed => Span::from("✓").fg(Color::Green), - StepStatus::InProgress => Span::from("▶").fg(Color::Yellow).bold(), - StepStatus::Pending => Span::from("○").fg(Color::Gray), + let (box_span, text_span) = match status { + StepStatus::Completed => ( + Span::styled("✔", Style::default().fg(Color::Green)), + Span::styled( + step, + Style::default() + .fg(Color::Gray) + .add_modifier(Modifier::CROSSED_OUT | Modifier::DIM), + ), + ), + StepStatus::InProgress => ( + Span::raw("□"), + Span::styled( + step, + Style::default() + .fg(Color::Blue) + .add_modifier(Modifier::BOLD), + ), + ), + StepStatus::Pending => ( + Span::raw("□"), + Span::styled( + step, + Style::default().fg(Color::Gray).add_modifier(Modifier::DIM), + ), + ), + }; + let prefix = if idx == 0 { + Span::raw(" ⎿ ") + } else { + Span::raw(" ") }; lines.push(Line::from(vec![ - format!("{num:>2}. [").into(), - icon_span, - "] ".into(), - step.into(), + prefix, + box_span, + Span::raw(" "), + text_span, ])); } } From cd5f9074afe4e76c0be477479e78f50eb3e94c11 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 7 Aug 2025 00:17:00 -0700 Subject: [PATCH 0065/1309] feat: add /tmp by default (#1919) Replaces the `include_default_writable_roots` option on `sandbox_workspace_write` (that defaulted to `true`, which was slightly weird/annoying) with `exclude_tmpdir_env_var`, which defaults to `false`. Though perhaps more importantly `/tmp` is now enabled by default as part of `sandbox_mode = "workspace-write"`, though `exclude_slash_tmp = false` can be used to disable this. --- codex-rs/common/src/sandbox_summary.rs | 28 ++++++----- codex-rs/config.md | 9 ++-- codex-rs/core/src/config.rs | 23 ++++++--- codex-rs/core/src/config_types.rs | 4 ++ codex-rs/core/src/protocol.rs | 61 ++++++++++++++--------- codex-rs/core/src/seatbelt.rs | 63 ++++++++++++++++-------- codex-rs/core/tests/sandbox.rs | 6 ++- codex-rs/linux-sandbox/tests/landlock.rs | 6 ++- 8 files changed, 131 insertions(+), 69 deletions(-) diff --git a/codex-rs/common/src/sandbox_summary.rs b/codex-rs/common/src/sandbox_summary.rs index e0e309a9d9..66e00cd451 100644 --- a/codex-rs/common/src/sandbox_summary.rs +++ b/codex-rs/common/src/sandbox_summary.rs @@ -7,22 +7,26 @@ pub fn summarize_sandbox_policy(sandbox_policy: &SandboxPolicy) -> String { SandboxPolicy::WorkspaceWrite { writable_roots, network_access, - include_default_writable_roots, + exclude_tmpdir_env_var, + exclude_slash_tmp, } => { let mut summary = "workspace-write".to_string(); - if !writable_roots.is_empty() { - summary.push_str(&format!( - " [{}]", - writable_roots - .iter() - .map(|p| p.to_string_lossy()) - .collect::>() - .join(", ") - )); + + let mut writable_entries = Vec::::new(); + writable_entries.push("workdir".to_string()); + if !*exclude_slash_tmp { + writable_entries.push("/tmp".to_string()); } - if !*include_default_writable_roots { - summary.push_str(" (exact writable roots)"); + if !*exclude_tmpdir_env_var { + writable_entries.push("$TMPDIR".to_string()); } + writable_entries.extend( + writable_roots + .iter() + .map(|p| p.to_string_lossy().to_string()), + ); + + summary.push_str(&format!(" [{}]", writable_entries.join(", "))); if *network_access { summary.push_str(" (network access enabled)"); } diff --git a/codex-rs/config.md b/codex-rs/config.md index f93a35ebca..e044684426 100644 --- a/codex-rs/config.md +++ b/codex-rs/config.md @@ -275,9 +275,12 @@ sandbox_mode = "workspace-write" # Extra settings that only apply when `sandbox = "workspace-write"`. [sandbox_workspace_write] -# By default, only the cwd for the Codex session will be writable (and $TMPDIR -# on macOS), but you can specify additional writable folders in this array. -writable_roots = ["/tmp"] +# By default, the cwd for the Codex session will be writable as well as $TMPDIR +# if set) and /tmp (if it exists). Setting the respective options to `true` +# will override those defaults. +exclude_tmpdir_env_var = false +exclude_slash_tmp = false + # Allow the command being run inside the sandbox to make outbound network # requests. Disabled by default. network_access = false diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 63a2e5949f..a2e0618a7f 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -361,10 +361,16 @@ impl ConfigToml { match resolved_sandbox_mode { SandboxMode::ReadOnly => SandboxPolicy::new_read_only_policy(), SandboxMode::WorkspaceWrite => match self.sandbox_workspace_write.as_ref() { - Some(s) => SandboxPolicy::WorkspaceWrite { - writable_roots: s.writable_roots.clone(), - network_access: s.network_access, - include_default_writable_roots: true, + Some(SandboxWorkplaceWrite { + writable_roots, + network_access, + exclude_tmpdir_env_var, + exclude_slash_tmp, + }) => SandboxPolicy::WorkspaceWrite { + writable_roots: writable_roots.clone(), + network_access: *network_access, + exclude_tmpdir_env_var: *exclude_tmpdir_env_var, + exclude_slash_tmp: *exclude_slash_tmp, }, None => SandboxPolicy::new_workspace_write_policy(), }, @@ -745,8 +751,10 @@ sandbox_mode = "workspace-write" [sandbox_workspace_write] writable_roots = [ - "/tmp", + "/my/workspace", ] +exclude_tmpdir_env_var = true +exclude_slash_tmp = true "#; let sandbox_workspace_write_cfg = toml::from_str::(sandbox_workspace_write) @@ -754,9 +762,10 @@ writable_roots = [ let sandbox_mode_override = None; assert_eq!( SandboxPolicy::WorkspaceWrite { - writable_roots: vec![PathBuf::from("/tmp")], + writable_roots: vec![PathBuf::from("/my/workspace")], network_access: false, - include_default_writable_roots: true, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, }, sandbox_workspace_write_cfg.derive_sandbox_policy(sandbox_mode_override) ); diff --git a/codex-rs/core/src/config_types.rs b/codex-rs/core/src/config_types.rs index 9bf0d483e1..a81c20502b 100644 --- a/codex-rs/core/src/config_types.rs +++ b/codex-rs/core/src/config_types.rs @@ -98,6 +98,10 @@ pub struct SandboxWorkplaceWrite { pub writable_roots: Vec, #[serde(default)] pub network_access: bool, + #[serde(default)] + pub exclude_tmpdir_env_var: bool, + #[serde(default)] + pub exclude_slash_tmp: bool, } #[derive(Deserialize, Debug, Clone, PartialEq, Default)] diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs index 052806dd97..e61fc0c3dc 100644 --- a/codex-rs/core/src/protocol.rs +++ b/codex-rs/core/src/protocol.rs @@ -185,11 +185,16 @@ pub enum SandboxPolicy { #[serde(default)] network_access: bool, - /// When set to `true`, will include defaults like the current working - /// directory and TMPDIR (on macOS). When `false`, only `writable_roots` - /// are used. (Mainly used for testing.) - #[serde(default = "default_true")] - include_default_writable_roots: bool, + /// When set to `true`, will NOT include the per-user `TMPDIR` + /// environment variable among the default writable roots. Defaults to + /// `false`. + #[serde(default)] + exclude_tmpdir_env_var: bool, + + /// When set to `true`, will NOT include the `/tmp` among the default + /// writable roots on UNIX. Defaults to `false`. + #[serde(default)] + exclude_slash_tmp: bool, }, } @@ -203,10 +208,6 @@ pub struct WritableRoot { pub read_only_subpaths: Vec, } -fn default_true() -> bool { - true -} - impl FromStr for SandboxPolicy { type Err = serde_json::Error; @@ -228,7 +229,8 @@ impl SandboxPolicy { SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: false, - include_default_writable_roots: true, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, } } @@ -263,27 +265,40 @@ impl SandboxPolicy { SandboxPolicy::ReadOnly => Vec::new(), SandboxPolicy::WorkspaceWrite { writable_roots, - include_default_writable_roots, - .. + exclude_tmpdir_env_var, + exclude_slash_tmp, + network_access: _, } => { // Start from explicitly configured writable roots. let mut roots: Vec = writable_roots.clone(); - // Optionally include defaults (cwd and TMPDIR on macOS). - if *include_default_writable_roots { - roots.push(cwd.to_path_buf()); + // Always include defaults: cwd, /tmp (if present on Unix), and + // on macOS, the per-user TMPDIR unless explicitly excluded. + roots.push(cwd.to_path_buf()); - // Also include the per-user tmp dir on macOS. - // Note this is added dynamically rather than storing it in - // `writable_roots` because `writable_roots` contains only static - // values deserialized from the config file. - if cfg!(target_os = "macos") { - if let Some(tmpdir) = std::env::var_os("TMPDIR") { - roots.push(PathBuf::from(tmpdir)); - } + // Include /tmp on Unix unless explicitly excluded. + if cfg!(unix) && !exclude_slash_tmp { + let slash_tmp = PathBuf::from("/tmp"); + if slash_tmp.is_dir() { + roots.push(slash_tmp); } } + // Include $TMPDIR unless explicitly excluded. On macOS, TMPDIR + // is per-user, so writes to TMPDIR should not be readable by + // other users on the system. + // + // By comparison, TMPDIR is not guaranteed to be defined on + // Linux or Windows, but supporting it here gives users a way to + // provide the model with their own temporary directory without + // having to hardcode it in the config. + if !exclude_tmpdir_env_var + && let Some(tmpdir) = std::env::var_os("TMPDIR") + && !tmpdir.is_empty() + { + roots.push(PathBuf::from(tmpdir)); + } + // For each root, compute subpaths that should remain read-only. roots .into_iter() diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index 0364840b1a..ff9dbaa7f9 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -134,6 +134,11 @@ mod tests { #[test] fn create_seatbelt_args_with_read_only_git_subpath() { + if cfg!(target_os = "windows") { + // /tmp does not exist on Windows, so skip this test. + return; + } + // Create a temporary workspace with two writable roots: one containing // a top-level .git directory and one without it. let tmp = TempDir::new().expect("tempdir"); @@ -144,19 +149,21 @@ mod tests { root_with_git_git_canon, root_without_git_canon, } = populate_tmpdir(tmp.path()); + let cwd = tmp.path().join("cwd"); // Build a policy that only includes the two test roots as writable and - // does not automatically include defaults like cwd or TMPDIR. + // does not automatically include defaults TMPDIR or /tmp. let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![root_with_git.clone(), root_without_git.clone()], network_access: false, - include_default_writable_roots: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, }; let args = create_seatbelt_command_args( vec!["/bin/echo".to_string(), "hello".to_string()], &policy, - tmp.path(), + &cwd, ); // Build the expected policy text using a raw string for readability. @@ -169,12 +176,12 @@ mod tests { ; allow read-only file operations (allow file-read*) (allow file-write* -(require-all (subpath (param "WRITABLE_ROOT_0")) (require-not (subpath (param "WRITABLE_ROOT_0_RO_0"))) ) (subpath (param "WRITABLE_ROOT_1")) +(require-all (subpath (param "WRITABLE_ROOT_0")) (require-not (subpath (param "WRITABLE_ROOT_0_RO_0"))) ) (subpath (param "WRITABLE_ROOT_1")) (subpath (param "WRITABLE_ROOT_2")) ) "#, ); - let expected_args = vec![ + let mut expected_args = vec![ "-p".to_string(), expected_policy, format!( @@ -189,16 +196,25 @@ mod tests { "-DWRITABLE_ROOT_1={}", root_without_git_canon.to_string_lossy() ), + format!("-DWRITABLE_ROOT_2={}", cwd.to_string_lossy()), + ]; + + expected_args.extend(vec![ "--".to_string(), "/bin/echo".to_string(), "hello".to_string(), - ]; + ]); - assert_eq!(args, expected_args); + assert_eq!(expected_args, args); } #[test] fn create_seatbelt_args_for_cwd_as_git_repo() { + if cfg!(target_os = "windows") { + // /tmp does not exist on Windows, so skip this test. + return; + } + // Create a temporary workspace with two writable roots: one containing // a top-level .git directory and one without it. let tmp = TempDir::new().expect("tempdir"); @@ -215,7 +231,8 @@ mod tests { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], network_access: false, - include_default_writable_roots: true, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, }; let args = create_seatbelt_command_args( @@ -224,17 +241,14 @@ mod tests { root_with_git.as_path(), ); - let tmpdir_env_var = if cfg!(target_os = "macos") { - std::env::var("TMPDIR") - .ok() - .map(PathBuf::from) - .and_then(|p| p.canonicalize().ok()) - .map(|p| p.to_string_lossy().to_string()) - } else { - None - }; + let tmpdir_env_var = std::env::var("TMPDIR") + .ok() + .map(PathBuf::from) + .and_then(|p| p.canonicalize().ok()) + .map(|p| p.to_string_lossy().to_string()); + let tempdir_policy_entry = if tmpdir_env_var.is_some() { - " (subpath (param \"WRITABLE_ROOT_1\"))" + r#" (subpath (param "WRITABLE_ROOT_2"))"# } else { "" }; @@ -249,7 +263,7 @@ mod tests { ; allow read-only file operations (allow file-read*) (allow file-write* -(require-all (subpath (param "WRITABLE_ROOT_0")) (require-not (subpath (param "WRITABLE_ROOT_0_RO_0"))) ){tempdir_policy_entry} +(require-all (subpath (param "WRITABLE_ROOT_0")) (require-not (subpath (param "WRITABLE_ROOT_0_RO_0"))) ) (subpath (param "WRITABLE_ROOT_1")){tempdir_policy_entry} ) "#, ); @@ -265,10 +279,17 @@ mod tests { "-DWRITABLE_ROOT_0_RO_0={}", root_with_git_git_canon.to_string_lossy() ), + format!( + "-DWRITABLE_ROOT_1={}", + PathBuf::from("/tmp") + .canonicalize() + .expect("canonicalize /tmp") + .to_string_lossy() + ), ]; if let Some(p) = tmpdir_env_var { - expected_args.push(format!("-DWRITABLE_ROOT_1={p}")); + expected_args.push(format!("-DWRITABLE_ROOT_2={p}")); } expected_args.extend(vec![ @@ -277,7 +298,7 @@ mod tests { "hello".to_string(), ]); - assert_eq!(args, expected_args); + assert_eq!(expected_args, args); } struct PopulatedTmp { diff --git a/codex-rs/core/tests/sandbox.rs b/codex-rs/core/tests/sandbox.rs index e85156bf05..ae5bdc44a6 100644 --- a/codex-rs/core/tests/sandbox.rs +++ b/codex-rs/core/tests/sandbox.rs @@ -76,7 +76,8 @@ async fn if_parent_of_repo_is_writable_then_dot_git_folder_is_writable() { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![test_scenario.repo_parent.clone()], network_access: false, - include_default_writable_roots: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, }; test_scenario @@ -101,7 +102,8 @@ async fn if_git_repo_is_writable_root_then_dot_git_folder_is_read_only() { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![test_scenario.repo_root.clone()], network_access: false, - include_default_writable_roots: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, }; test_scenario diff --git a/codex-rs/linux-sandbox/tests/landlock.rs b/codex-rs/linux-sandbox/tests/landlock.rs index 041e64e208..96298c6563 100644 --- a/codex-rs/linux-sandbox/tests/landlock.rs +++ b/codex-rs/linux-sandbox/tests/landlock.rs @@ -51,7 +51,11 @@ async fn run_cmd(cmd: &[&str], writable_roots: &[PathBuf], timeout_ms: u64) { let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: writable_roots.to_vec(), network_access: false, - include_default_writable_roots: true, + // Exclude tmp-related folders from writable roots because we need a + // folder that is writable by tests but that we intentionally disallow + // writing to in the sandbox. + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, }; let sandbox_program = env!("CARGO_BIN_EXE_codex-linux-sandbox"); let codex_linux_sandbox_exe = Some(PathBuf::from(sandbox_program)); From 4e29c4afe4b26f444d062fe22d7e590a9c499eff Mon Sep 17 00:00:00 2001 From: easong-openai Date: Thu, 7 Aug 2025 00:41:48 -0700 Subject: [PATCH 0066/1309] Add a UI hint when you press @ (#1903) This will make @ more discoverable (even though it is currently not super useful, IMO it should be used to bring files into context from outside CWD) --------- Co-authored-by: Gabriel Peal --- codex-rs/tui/src/app.rs | 4 +- codex-rs/tui/src/bottom_pane/chat_composer.rs | 44 ++++++++++++++----- .../tui/src/bottom_pane/file_search_popup.rs | 11 +++++ 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index ad3b4f3372..3ecb4d0fcf 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -416,7 +416,9 @@ impl App<'_> { } } AppEvent::StartFileSearch(query) => { - self.file_search.on_user_query(query); + if !query.is_empty() { + self.file_search.on_user_query(query); + } } AppEvent::FileSearchResult { query, matches } => { if let AppState::Chat { widget } = &mut self.app_state { diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 5d877253a6..2feab9a2f2 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -331,8 +331,9 @@ impl ChatComposer { /// - The cursor may be anywhere *inside* the token (including on the /// leading `@`). It does **not** need to be at the end of the line. /// - A token is delimited by ASCII whitespace (space, tab, newline). - /// - If the token under the cursor starts with `@` and contains at least - /// one additional character, that token (without `@`) is returned. + /// - If the token under the cursor starts with `@`, that token is + /// returned without the leading `@`. This includes the case where the + /// token is just "@" (empty query), which is used to trigger a UI hint fn current_at_token(textarea: &TextArea) -> Option { let cursor_offset = textarea.cursor(); let text = textarea.text(); @@ -403,14 +404,20 @@ impl ChatComposer { }; let left_at = token_left - .filter(|t| t.starts_with('@') && t.len() > 1) + .filter(|t| t.starts_with('@')) .map(|t| t[1..].to_string()); let right_at = token_right - .filter(|t| t.starts_with('@') && t.len() > 1) + .filter(|t| t.starts_with('@')) .map(|t| t[1..].to_string()); if at_whitespace { - return right_at.or(left_at); + if right_at.is_some() { + return right_at; + } + if token_left.is_some_and(|t| t == "@") { + return None; + } + return left_at; } if after_cursor.starts_with('@') { return right_at.or(left_at); @@ -607,16 +614,26 @@ impl ChatComposer { return; } - self.app_event_tx - .send(AppEvent::StartFileSearch(query.clone())); + if !query.is_empty() { + self.app_event_tx + .send(AppEvent::StartFileSearch(query.clone())); + } match &mut self.active_popup { ActivePopup::File(popup) => { - popup.set_query(&query); + if query.is_empty() { + popup.set_empty_prompt(); + } else { + popup.set_query(&query); + } } _ => { let mut popup = FileSearchPopup::new(); - popup.set_query(&query); + if query.is_empty() { + popup.set_empty_prompt(); + } else { + popup.set_query(&query); + } self.active_popup = ActivePopup::File(popup); } } @@ -773,7 +790,12 @@ mod tests { ("@👍", 2, Some("👍".to_string()), "Emoji token"), // Invalid cases (should return None) ("hello", 2, None, "No @ symbol"), - ("@", 1, None, "Only @ symbol"), + ( + "@", + 1, + Some("".to_string()), + "Only @ symbol triggers empty query", + ), ("@ hello", 2, None, "@ followed by space"), ("test @ world", 6, None, "@ with spaces around"), ]; @@ -807,7 +829,7 @@ mod tests { "Second token", ), // Edge cases - ("@", 0, None, "Only @ symbol"), + ("@", 0, Some("".to_string()), "Only @ symbol"), ("@a", 2, Some("a".to_string()), "Single character after @"), ("", 0, None, "Empty input"), ]; diff --git a/codex-rs/tui/src/bottom_pane/file_search_popup.rs b/codex-rs/tui/src/bottom_pane/file_search_popup.rs index c30a24f984..a811a22a8c 100644 --- a/codex-rs/tui/src/bottom_pane/file_search_popup.rs +++ b/codex-rs/tui/src/bottom_pane/file_search_popup.rs @@ -54,6 +54,17 @@ impl FileSearchPopup { } } + /// Put the popup into an "idle" state used for an empty query (just "@"). + /// Shows a hint instead of matches until the user types more characters. + pub(crate) fn set_empty_prompt(&mut self) { + self.display_query.clear(); + self.pending_query.clear(); + self.waiting = false; + self.matches.clear(); + // Reset selection/scroll state when showing the empty prompt. + self.state.reset(); + } + /// Replace matches when a `FileSearchResult` arrives. /// Replace matches. Only applied when `query` matches `pending_query`. pub(crate) fn set_matches(&mut self, query: &str, matches: Vec) { From 04b40ac1791d2ce89181a3803a53ea3a0f0d1f2e Mon Sep 17 00:00:00 2001 From: aibrahim-oai Date: Thu, 7 Aug 2025 00:45:47 -0700 Subject: [PATCH 0067/1309] Move used tokens next to the hints (#1930) Before: image After: image --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 2feab9a2f2..ea3a4eddbe 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -8,6 +8,7 @@ use ratatui::layout::Layout; use ratatui::layout::Margin; use ratatui::layout::Rect; use ratatui::style::Color; +use ratatui::style::Modifier; use ratatui::style::Style; use ratatui::style::Styled; use ratatui::style::Stylize; @@ -666,7 +667,7 @@ impl WidgetRef for &ChatComposer { ActivePopup::None => { let bottom_line_rect = popup_rect; let key_hint_style = Style::default().fg(Color::Cyan); - let hint = if self.ctrl_c_quit_hint { + let mut hint = if self.ctrl_c_quit_hint { vec![ Span::from(" "), "Ctrl+C again".set_style(key_hint_style), @@ -688,6 +689,31 @@ impl WidgetRef for &ChatComposer { Span::from(" quit"), ] }; + + // Append token/context usage info to the footer hints when available. + if let Some(token_usage_info) = &self.token_usage_info { + let token_usage = &token_usage_info.token_usage; + hint.push(Span::from(" ")); + hint.push( + Span::from(format!("{} tokens used", token_usage.total_tokens)) + .style(Style::default().add_modifier(Modifier::DIM)), + ); + if let Some(context_window) = token_usage_info.model_context_window { + let percent_remaining: u8 = if context_window > 0 { + let percent = 100.0 + - (token_usage.total_tokens as f32 / context_window as f32 * 100.0); + percent.clamp(0.0, 100.0) as u8 + } else { + 100 + }; + hint.push(Span::from(" ")); + hint.push( + Span::from(format!("{percent_remaining}% context left")) + .style(Style::default().add_modifier(Modifier::DIM)), + ); + } + } + Line::from(hint) .style(Style::default().dim()) .render_ref(bottom_line_rect, buf); @@ -712,34 +738,7 @@ impl WidgetRef for &ChatComposer { let mut state = self.textarea_state.borrow_mut(); StatefulWidgetRef::render_ref(&(&self.textarea), textarea_rect, buf, &mut state); if self.textarea.text().is_empty() { - let placeholder = if let Some(token_usage_info) = &self.token_usage_info { - let token_usage = &token_usage_info.token_usage; - let model_context_window = token_usage_info.model_context_window; - match (token_usage.total_tokens, model_context_window) { - (total_tokens, Some(context_window)) => { - let percent_remaining: u8 = if context_window > 0 { - // Calculate the percentage of context left. - let percent = - 100.0 - (total_tokens as f32 / context_window as f32 * 100.0); - percent.clamp(0.0, 100.0) as u8 - } else { - // If we don't have a context window, we cannot compute the - // percentage. - 100 - }; - // When https://github.com/openai/codex/issues/1257 is resolved, - // check if `percent_remaining < 25`, and if so, recommend - // /compact. - format!("{BASE_PLACEHOLDER_TEXT} — {percent_remaining}% context left") - } - (total_tokens, None) => { - format!("{BASE_PLACEHOLDER_TEXT} — {total_tokens} tokens used") - } - } - } else { - BASE_PLACEHOLDER_TEXT.to_string() - }; - Line::from(placeholder) + Line::from(BASE_PLACEHOLDER_TEXT) .style(Style::default().dim()) .render_ref(textarea_rect.inner(Margin::new(1, 0)), buf); } From eb80614a7c6a83abb7f03e5cb45f98d0b75ff343 Mon Sep 17 00:00:00 2001 From: Ed Bayes Date: Thu, 7 Aug 2025 00:46:45 -0700 Subject: [PATCH 0068/1309] Tint chat composer background (#1921) ## Summary - give the chat composer a subtle custom background and apply it across the full area drawn composer-bg - update turn interrupted to be more human readable CleanShot 2025-08-06 at 22 44 47@2x ## Testing - `cargo test --all-features` *(fails: `let` expressions in `core/src/client.rs` require newer rustc)* - `just fix` *(fails: `let` expressions in `core/src/client.rs` require newer rustc)* ------ https://chatgpt.com/codex/tasks/task_i_68941f32c1008322bbcc39ee1d29a526 --- codex-rs/core/src/codex.rs | 2 +- codex-rs/tui/src/bottom_pane/chat_composer.rs | 6 ++++++ codex-rs/tui/src/history_cell.rs | 6 ++---- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 4a4faa84ee..eb1bc4f9d8 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -688,7 +688,7 @@ impl AgentTask { let event = Event { id: self.sub_id, msg: EventMsg::Error(ErrorEvent { - message: "Turn interrupted".to_string(), + message: " Turn interrupted".to_string(), }), }; let tx_event = self.sess.tx_event.clone(); diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index ea3a4eddbe..e53fe03676 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -35,6 +35,8 @@ const BASE_PLACEHOLDER_TEXT: &str = "..."; /// If the pasted content exceeds this number of characters, replace it with a /// placeholder in the UI. const LARGE_PASTE_CHAR_THRESHOLD: usize = 1000; +/// Background color used for the chat composer area. +const COMPOSER_BG_COLOR: Color = Color::Black; /// Result returned when the user interacts with the text area. pub enum InputResult { @@ -735,6 +737,10 @@ impl WidgetRef for &ChatComposer { let mut textarea_rect = textarea_rect; textarea_rect.width = textarea_rect.width.saturating_sub(1); textarea_rect.x += 1; + + // Fill only the textarea content region with a subtle background so it + // doesn't affect the hint line or popups and remains behind the text. + buf.set_style(textarea_rect, Style::default().bg(COMPOSER_BG_COLOR)); let mut state = self.textarea_state.borrow_mut(); StatefulWidgetRef::render_ref(&(&self.textarea), textarea_rect, buf, &mut state); if self.textarea.text().is_empty() { diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index be96a9e9ef..8df5340f85 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -504,10 +504,8 @@ impl HistoryCell { } pub(crate) fn new_error_event(message: String) -> Self { - let lines: Vec> = vec![ - vec!["ERROR: ".red().bold(), message.into()].into(), - "".into(), - ]; + let lines: Vec> = + vec![vec!["🖐 ".red().bold(), message.into()].into(), "".into()]; HistoryCell::ErrorEvent { view: TextBlock::new(lines), } From 28395df957dbbe2acd4944ef4c42501eb013f8d3 Mon Sep 17 00:00:00 2001 From: ae Date: Thu, 7 Aug 2025 01:13:36 -0700 Subject: [PATCH 0069/1309] [fix] fix absolute and % token counts (#1931) - For absolute, use non-cached input + output. - For estimating what % of the model's context window is used, we need to account for reasoning output tokens from prior turns being dropped from the context window. We approximate this here by subtracting reasoning output tokens from the total. This will be off for the current turn and pending function calls. We can improve it later. --- codex-rs/core/src/protocol.rs | 41 +++++++++++++++---- .../src/event_processor_with_human_output.rs | 5 +-- codex-rs/tui/src/history_cell.rs | 18 ++------ 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs index e61fc0c3dc..c789798bcd 100644 --- a/codex-rs/core/src/protocol.rs +++ b/codex-rs/core/src/protocol.rs @@ -448,6 +448,28 @@ impl TokenUsage { pub fn is_zero(&self) -> bool { self.total_tokens == 0 } + + pub fn cached_input(&self) -> u64 { + self.cached_input_tokens.unwrap_or(0) + } + + pub fn non_cached_input(&self) -> u64 { + self.input_tokens.saturating_sub(self.cached_input()) + } + + /// Primary count for display as a single absolute value: non-cached input + output. + pub fn blended_total(&self) -> u64 { + self.non_cached_input() + self.output_tokens + } + + /// For estimating what % of the model's context window is used, we need to account + /// for reasoning output tokens from prior turns being dropped from the context window. + /// We approximate this here by subtracting reasoning output tokens from the total. + /// This will be off for the current turn and pending function calls. + pub fn tokens_in_context_window(&self) -> u64 { + self.total_tokens + .saturating_sub(self.reasoning_output_tokens.unwrap_or(0)) + } } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -463,17 +485,20 @@ impl From for FinalOutput { impl fmt::Display for FinalOutput { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let u = &self.token_usage; + let token_usage = &self.token_usage; write!( f, "Token usage: total={} input={}{} output={}{}", - u.total_tokens, - u.input_tokens, - u.cached_input_tokens - .map(|c| format!(" (cached {c})")) - .unwrap_or_default(), - u.output_tokens, - u.reasoning_output_tokens + token_usage.blended_total(), + token_usage.non_cached_input(), + if token_usage.cached_input() > 0 { + format!(" (+ {} cached)", token_usage.cached_input()) + } else { + String::new() + }, + token_usage.output_tokens, + token_usage + .reasoning_output_tokens .map(|r| format!(" (reasoning {r})")) .unwrap_or_default() ) diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 6b03ed7882..a2ae813183 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -21,7 +21,6 @@ use codex_core::protocol::PatchApplyBeginEvent; use codex_core::protocol::PatchApplyEndEvent; use codex_core::protocol::SessionConfiguredEvent; use codex_core::protocol::TaskCompleteEvent; -use codex_core::protocol::TokenUsage; use codex_core::protocol::TurnDiffEvent; use owo_colors::OwoColorize; use owo_colors::Style; @@ -183,8 +182,8 @@ impl EventProcessor for EventProcessorWithHumanOutput { } return CodexStatus::InitiateShutdown; } - EventMsg::TokenCount(TokenUsage { total_tokens, .. }) => { - ts_println!(self, "tokens used: {total_tokens}"); + EventMsg::TokenCount(token_usage) => { + ts_println!(self, "tokens used: {}", token_usage.blended_total()); } EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta }) => { if !self.answer_started { diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 8df5340f85..6beb79759f 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -474,27 +474,17 @@ impl HistoryCell { lines.push(Line::from("token usage".bold())); lines.push(Line::from(vec![ " input: ".bold(), - usage.input_tokens.to_string().into(), - ])); - lines.push(Line::from(vec![ - " cached input: ".bold(), - usage.cached_input_tokens.unwrap_or(0).to_string().into(), + usage.non_cached_input().to_string().into(), + " ".into(), + format!("(+ {} cached)", usage.cached_input()).into(), ])); lines.push(Line::from(vec![ " output: ".bold(), usage.output_tokens.to_string().into(), ])); - lines.push(Line::from(vec![ - " reasoning output: ".bold(), - usage - .reasoning_output_tokens - .unwrap_or(0) - .to_string() - .into(), - ])); lines.push(Line::from(vec![ " total: ".bold(), - usage.total_tokens.to_string().into(), + usage.blended_total().to_string().into(), ])); lines.push(Line::from("")); From 6d19b73edf0f58e7ab2e5288d4048a640623eb45 Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Thu, 7 Aug 2025 01:17:33 -0700 Subject: [PATCH 0070/1309] Add logout command to CLI and TUI (#1932) ## Summary - support `codex logout` via new subcommand and helper that removes the stored `auth.json` - expose a `logout` function in `codex-login` and test it - add `/logout` slash command in the TUI; command list is filtered when not logged in and the handler deletes `auth.json` then exits ## Testing - `just fix` *(fails: failed to get `diffy` from crates.io)* - `cargo test --all-features` *(fails: failed to get `diffy` from crates.io)* ------ https://chatgpt.com/codex/tasks/task_i_68945c3facac832ca83d48499716fb51 --- codex-rs/cli/src/login.rs | 20 ++++++++++++++++++++ codex-rs/cli/src/main.rs | 14 ++++++++++++++ codex-rs/login/src/lib.rs | 23 +++++++++++++++++++++++ codex-rs/tui/src/app.rs | 6 ++++++ codex-rs/tui/src/slash_command.rs | 2 ++ 5 files changed, 65 insertions(+) diff --git a/codex-rs/cli/src/login.rs b/codex-rs/cli/src/login.rs index 4fa13f0cc6..4291e06820 100644 --- a/codex-rs/cli/src/login.rs +++ b/codex-rs/cli/src/login.rs @@ -8,6 +8,7 @@ use codex_login::OPENAI_API_KEY_ENV_VAR; use codex_login::load_auth; use codex_login::login_with_api_key; use codex_login::login_with_chatgpt; +use codex_login::logout; pub async fn run_login_with_chatgpt(cli_config_overrides: CliConfigOverrides) -> ! { let config = load_config_or_exit(cli_config_overrides); @@ -80,6 +81,25 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! { } } +pub async fn run_logout(cli_config_overrides: CliConfigOverrides) -> ! { + let config = load_config_or_exit(cli_config_overrides); + + match logout(&config.codex_home) { + Ok(true) => { + eprintln!("Successfully logged out"); + std::process::exit(0); + } + Ok(false) => { + eprintln!("Not logged in"); + std::process::exit(0); + } + Err(e) => { + eprintln!("Error logging out: {e}"); + std::process::exit(1); + } + } +} + fn load_config_or_exit(cli_config_overrides: CliConfigOverrides) -> Config { let cli_overrides = match cli_config_overrides.parse_overrides() { Ok(v) => v, diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index c43365c7d5..9aef22c09f 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -10,6 +10,7 @@ use codex_cli::SeatbeltCommand; use codex_cli::login::run_login_status; use codex_cli::login::run_login_with_api_key; use codex_cli::login::run_login_with_chatgpt; +use codex_cli::login::run_logout; use codex_cli::proto; use codex_common::CliConfigOverrides; use codex_exec::Cli as ExecCli; @@ -48,6 +49,9 @@ enum Subcommand { /// Manage login. Login(LoginCommand), + /// Remove stored authentication credentials. + Logout(LogoutCommand), + /// Experimental: run Codex as an MCP server. Mcp, @@ -106,6 +110,12 @@ enum LoginSubcommand { Status, } +#[derive(Debug, Parser)] +struct LogoutCommand { + #[clap(skip)] + config_overrides: CliConfigOverrides, +} + fn main() -> anyhow::Result<()> { arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move { cli_main(codex_linux_sandbox_exe).await?; @@ -147,6 +157,10 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() } } } + Some(Subcommand::Logout(mut logout_cli)) => { + prepend_config_flags(&mut logout_cli.config_overrides, cli.config_overrides); + run_logout(logout_cli.config_overrides).await; + } Some(Subcommand::Proto(mut proto_cli)) => { prepend_config_flags(&mut proto_cli.config_overrides, cli.config_overrides); proto::run_main(proto_cli).await?; diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index 95bc119ec5..f35191ced9 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -6,6 +6,7 @@ use serde::Serialize; use std::env; use std::fs::File; use std::fs::OpenOptions; +use std::fs::remove_file; use std::io::Read; use std::io::Write; #[cfg(unix)] @@ -185,6 +186,17 @@ fn get_auth_file(codex_home: &Path) -> PathBuf { codex_home.join("auth.json") } +/// Delete the auth.json file inside `codex_home` if it exists. Returns `Ok(true)` +/// if a file was removed, `Ok(false)` if no auth file was present. +pub fn logout(codex_home: &Path) -> std::io::Result { + let auth_file = get_auth_file(codex_home); + match remove_file(&auth_file) { + Ok(_) => Ok(true), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false), + Err(err) => Err(err), + } +} + /// Represents a running login subprocess. The child can be killed by holding /// the mutex and calling `kill()`. #[derive(Debug, Clone)] @@ -494,4 +506,15 @@ mod tests { assert!(auth.get_token_data().await.is_err()); } + + #[test] + fn logout_removes_auth_file() -> Result<(), std::io::Error> { + let dir = tempdir()?; + login_with_api_key(dir.path(), "sk-test-key")?; + assert!(dir.path().join("auth.json").exists()); + let removed = logout(dir.path())?; + assert!(removed); + assert!(!dir.path().join("auth.json").exists()); + Ok(()) + } } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 3ecb4d0fcf..1ba8883b0b 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -328,6 +328,12 @@ impl App<'_> { SlashCommand::Quit => { break; } + SlashCommand::Logout => { + if let Err(e) = codex_login::logout(&self.config.codex_home) { + tracing::error!("failed to logout: {e}"); + } + break; + } SlashCommand::Diff => { let (is_git_repo, diff_text) = match get_git_diff() { Ok(v) => v, diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index 75bca641ac..0513d64443 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -17,6 +17,7 @@ pub enum SlashCommand { Compact, Diff, Status, + Logout, Quit, #[cfg(debug_assertions)] TestApproval, @@ -32,6 +33,7 @@ impl SlashCommand { SlashCommand::Quit => "Exit the application", SlashCommand::Diff => "Show git diff (including untracked files)", SlashCommand::Status => "Show current session configuration and token usage", + SlashCommand::Logout => "Log out of Codex", #[cfg(debug_assertions)] SlashCommand::TestApproval => "Test approval request", } From 0334476894ef8fd02a5dda1c9d80b9ec82d36d25 Mon Sep 17 00:00:00 2001 From: ae Date: Thu, 7 Aug 2025 01:27:45 -0700 Subject: [PATCH 0071/1309] feat: parse info from auth.json and show in /status (#1923) - `/status` renders ``` signed in with chatgpt login: example@example.com plan: plus ``` - Setup for using this info in a few more places. --------- Co-authored-by: Michael Bolin --- codex-rs/Cargo.lock | 3 + codex-rs/core/tests/client.rs | 26 +++---- codex-rs/login/Cargo.toml | 3 + codex-rs/login/src/lib.rs | 118 +++++++++++++++++++++++-------- codex-rs/login/src/token_data.rs | 117 ++++++++++++++++++++++++++++++ codex-rs/tui/src/history_cell.rs | 34 ++++++++- 6 files changed, 254 insertions(+), 47 deletions(-) create mode 100644 codex-rs/login/src/token_data.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index aa5398dde1..eabd9f35db 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -792,11 +792,14 @@ dependencies = [ name = "codex-login" version = "0.0.0" dependencies = [ + "base64 0.22.1", "chrono", + "pretty_assertions", "reqwest", "serde", "serde_json", "tempfile", + "thiserror 2.0.12", "tokio", ] diff --git a/codex-rs/core/tests/client.rs b/codex-rs/core/tests/client.rs index 60eb922474..2148e874bc 100644 --- a/codex-rs/core/tests/client.rs +++ b/codex-rs/core/tests/client.rs @@ -290,13 +290,10 @@ async fn chatgpt_auth_sends_correct_request() { let mut config = load_default_config_for_test(&codex_home); config.model_provider = model_provider; let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new()); - let CodexSpawnOk { codex, .. } = Codex::spawn( - config, - Some(auth_from_token("Access Token".to_string())), - ctrl_c.clone(), - ) - .await - .unwrap(); + let CodexSpawnOk { codex, .. } = + Codex::spawn(config, Some(create_dummy_codex_auth()), ctrl_c.clone()) + .await + .unwrap(); codex .submit(Op::UserInput { @@ -541,13 +538,10 @@ async fn env_var_overrides_loaded_auth() { config.model_provider = provider; let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new()); - let CodexSpawnOk { codex, .. } = Codex::spawn( - config, - Some(auth_from_token("Default Access Token".to_string())), - ctrl_c.clone(), - ) - .await - .unwrap(); + let CodexSpawnOk { codex, .. } = + Codex::spawn(config, Some(create_dummy_codex_auth()), ctrl_c.clone()) + .await + .unwrap(); codex .submit(Op::UserInput { @@ -561,7 +555,7 @@ async fn env_var_overrides_loaded_auth() { wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; } -fn auth_from_token(id_token: String) -> CodexAuth { +fn create_dummy_codex_auth() -> CodexAuth { CodexAuth::new( None, AuthMode::ChatGPT, @@ -569,7 +563,7 @@ fn auth_from_token(id_token: String) -> CodexAuth { Some(AuthDotJson { openai_api_key: None, tokens: Some(TokenData { - id_token, + id_token: Default::default(), access_token: "Access Token".to_string(), refresh_token: "test".to_string(), account_id: Some("account_id".to_string()), diff --git a/codex-rs/login/Cargo.toml b/codex-rs/login/Cargo.toml index 650291b3bc..a290c01eb6 100644 --- a/codex-rs/login/Cargo.toml +++ b/codex-rs/login/Cargo.toml @@ -7,10 +7,12 @@ version = { workspace = true } workspace = true [dependencies] +base64 = "0.22" chrono = { version = "0.4", features = ["serde"] } reqwest = { version = "0.12", features = ["json"] } serde = { version = "1", features = ["derive"] } serde_json = "1" +thiserror = "2.0.12" tokio = { version = "1", features = [ "io-std", "macros", @@ -20,4 +22,5 @@ tokio = { version = "1", features = [ ] } [dev-dependencies] +pretty_assertions = "1.4.1" tempfile = "3" diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index f35191ced9..a52e105628 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -20,6 +20,11 @@ use std::sync::Mutex; use std::time::Duration; use tokio::process::Command; +pub use crate::token_data::TokenData; +use crate::token_data::parse_id_token; + +mod token_data; + const SOURCE_FOR_PYTHON_SERVER: &str = include_str!("./login_with_chatgpt.py"); const CLIENT_ID: &str = "app_EMoamEEZ73f0CkXaXp7hrann"; @@ -182,7 +187,7 @@ pub fn load_auth(codex_home: &Path, include_env_var: bool) -> std::io::Result PathBuf { +pub fn get_auth_file(codex_home: &Path) -> PathBuf { codex_home.join("auth.json") } @@ -332,7 +337,7 @@ async fn update_tokens( let mut auth_dot_json = try_read_auth_json(auth_file)?; let tokens = auth_dot_json.tokens.get_or_insert_with(TokenData::default); - tokens.id_token = id_token.to_string(); + tokens.id_token = parse_id_token(&id_token).map_err(std::io::Error::other)?; if let Some(access_token) = access_token { tokens.access_token = access_token.to_string(); } @@ -403,22 +408,12 @@ pub struct AuthDotJson { pub last_refresh: Option>, } -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Default)] -pub struct TokenData { - /// This is a JWT. - pub id_token: String, - - /// This is a JWT. - pub access_token: String, - - pub refresh_token: String, - - pub account_id: Option, -} - #[cfg(test)] mod tests { use super::*; + use crate::token_data::IdTokenInfo; + use base64::Engine; + use pretty_assertions::assert_eq; use tempfile::tempdir; #[test] @@ -446,10 +441,35 @@ mod tests { } #[tokio::test] - #[expect(clippy::unwrap_used)] + #[expect(clippy::expect_used, clippy::unwrap_used)] async fn loads_token_data_from_auth_json() { let dir = tempdir().unwrap(); let auth_file = dir.path().join("auth.json"); + // Create a minimal valid JWT for the id_token field. + #[derive(Serialize)] + struct Header { + alg: &'static str, + typ: &'static str, + } + let header = Header { + alg: "none", + typ: "JWT", + }; + let payload = serde_json::json!({ + "email": "user@example.com", + "email_verified": true, + "https://api.openai.com/auth": { + "chatgpt_account_id": "bc3618e3-489d-4d49-9362-1561dc53ba53", + "chatgpt_plan_type": "pro", + "chatgpt_user_id": "user-12345", + "user_id": "user-12345", + } + }); + let b64 = |b: &[u8]| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b); + let header_b64 = b64(&serde_json::to_vec(&header).unwrap()); + let payload_b64 = b64(&serde_json::to_vec(&payload).unwrap()); + let signature_b64 = b64(b"sig"); + let fake_jwt = format!("{header_b64}.{payload_b64}.{signature_b64}"); std::fs::write( auth_file, format!( @@ -457,30 +477,68 @@ mod tests { {{ "OPENAI_API_KEY": null, "tokens": {{ - "id_token": "test-id-token", + "id_token": "{fake_jwt}", "access_token": "test-access-token", "refresh_token": "test-refresh-token" }}, - "last_refresh": "{}" + "last_refresh": "2025-08-06T20:41:36.232376Z" }} "#, - Utc::now().to_rfc3339() ), ) .unwrap(); - let auth = load_auth(dir.path(), false).unwrap().unwrap(); - assert_eq!(auth.mode, AuthMode::ChatGPT); - assert_eq!(auth.api_key, None); + let CodexAuth { + api_key, + mode, + auth_dot_json, + auth_file, + } = load_auth(dir.path(), false).unwrap().unwrap(); + assert_eq!(None, api_key); + assert_eq!(AuthMode::ChatGPT, mode); + assert_eq!(dir.path().join("auth.json"), auth_file); + + let guard = auth_dot_json.lock().unwrap(); + let auth_dot_json = guard.as_ref().expect("AuthDotJson should exist"); + assert_eq!( - auth.get_token_data().await.unwrap(), - TokenData { - id_token: "test-id-token".to_string(), - access_token: "test-access-token".to_string(), - refresh_token: "test-refresh-token".to_string(), - account_id: None, - } - ); + &AuthDotJson { + openai_api_key: None, + tokens: Some(TokenData { + id_token: IdTokenInfo { + email: Some("user@example.com".to_string()), + chatgpt_plan_type: Some("pro".to_string()), + }, + access_token: "test-access-token".to_string(), + refresh_token: "test-refresh-token".to_string(), + account_id: None, + }), + last_refresh: Some( + DateTime::parse_from_rfc3339("2025-08-06T20:41:36.232376Z") + .unwrap() + .with_timezone(&Utc) + ), + }, + auth_dot_json + ) + } + + #[test] + #[expect(clippy::expect_used, clippy::unwrap_used)] + fn id_token_info_handles_missing_fields() { + // Payload without email or plan should yield None values. + let header = serde_json::json!({"alg": "none", "typ": "JWT"}); + let payload = serde_json::json!({"sub": "123"}); + let header_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD + .encode(serde_json::to_vec(&header).unwrap()); + let payload_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD + .encode(serde_json::to_vec(&payload).unwrap()); + let signature_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b"sig"); + let jwt = format!("{header_b64}.{payload_b64}.{signature_b64}"); + + let info = parse_id_token(&jwt).expect("should parse"); + assert!(info.email.is_none()); + assert!(info.chatgpt_plan_type.is_none()); } #[tokio::test] diff --git a/codex-rs/login/src/token_data.rs b/codex-rs/login/src/token_data.rs new file mode 100644 index 0000000000..55b51b9d44 --- /dev/null +++ b/codex-rs/login/src/token_data.rs @@ -0,0 +1,117 @@ +use base64::Engine; +use serde::Deserialize; +use serde::Serialize; +use thiserror::Error; + +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Default)] +pub struct TokenData { + /// Flat info parsed from the JWT in auth.json. + #[serde(deserialize_with = "deserialize_id_token")] + pub id_token: IdTokenInfo, + + /// This is a JWT. + pub access_token: String, + + pub refresh_token: String, + + pub account_id: Option, +} + +/// Flat subset of useful claims in id_token from auth.json. +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] +pub struct IdTokenInfo { + pub email: Option, + /// The ChatGPT subscription plan type + /// (e.g., "free", "plus", "pro", "business", "enterprise", "edu"). + /// (Note: ae has not verified that those are the exact values.) + pub chatgpt_plan_type: Option, +} + +#[derive(Deserialize)] +struct IdClaims { + #[serde(default)] + email: Option, + #[serde(rename = "https://api.openai.com/auth", default)] + auth: Option, +} + +#[derive(Deserialize)] +struct AuthClaims { + #[serde(default)] + chatgpt_plan_type: Option, +} + +#[derive(Debug, Error)] +pub enum IdTokenInfoError { + #[error("invalid ID token format")] + InvalidFormat, + #[error(transparent)] + Base64(#[from] base64::DecodeError), + #[error(transparent)] + Json(#[from] serde_json::Error), +} + +pub(crate) fn parse_id_token(id_token: &str) -> Result { + // JWT format: header.payload.signature + let mut parts = id_token.split('.'); + let (_header_b64, payload_b64, _sig_b64) = match (parts.next(), parts.next(), parts.next()) { + (Some(h), Some(p), Some(s)) if !h.is_empty() && !p.is_empty() && !s.is_empty() => (h, p, s), + _ => return Err(IdTokenInfoError::InvalidFormat), + }; + + let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(payload_b64)?; + let claims: IdClaims = serde_json::from_slice(&payload_bytes)?; + + Ok(IdTokenInfo { + email: claims.email, + chatgpt_plan_type: claims.auth.and_then(|a| a.chatgpt_plan_type), + }) +} + +fn deserialize_id_token<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + parse_id_token(&s).map_err(serde::de::Error::custom) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::Serialize; + + #[test] + #[expect(clippy::expect_used, clippy::unwrap_used)] + fn id_token_info_parses_email_and_plan() { + // Build a fake JWT with a URL-safe base64 payload containing email and plan. + #[derive(Serialize)] + struct Header { + alg: &'static str, + typ: &'static str, + } + let header = Header { + alg: "none", + typ: "JWT", + }; + let payload = serde_json::json!({ + "email": "user@example.com", + "https://api.openai.com/auth": { + "chatgpt_plan_type": "pro" + } + }); + + fn b64url_no_pad(bytes: &[u8]) -> String { + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes) + } + + let header_b64 = b64url_no_pad(&serde_json::to_vec(&header).unwrap()); + let payload_b64 = b64url_no_pad(&serde_json::to_vec(&payload).unwrap()); + let signature_b64 = b64url_no_pad(b"sig"); + let fake_jwt = format!("{header_b64}.{payload_b64}.{signature_b64}"); + + let info = parse_id_token(&fake_jwt).expect("should parse"); + assert_eq!(info.email.as_deref(), Some("user@example.com")); + assert_eq!(info.chatgpt_plan_type.as_deref(), Some("pro")); + } +} diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 6beb79759f..d7a06fa576 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -15,6 +15,8 @@ use codex_core::protocol::FileChange; use codex_core::protocol::McpInvocation; use codex_core::protocol::SessionConfiguredEvent; use codex_core::protocol::TokenUsage; +use codex_login::get_auth_file; +use codex_login::try_read_auth_json; use image::DynamicImage; use image::ImageReader; use mcp_types::EmbeddedResourceResource; @@ -469,8 +471,38 @@ impl HistoryCell { lines.push(Line::from(vec![format!("{key}: ").bold(), value.into()])); } - // Token usage lines.push(Line::from("")); + + // Auth + let auth_file = get_auth_file(&config.codex_home); + if let Ok(auth) = try_read_auth_json(&auth_file) { + if auth.tokens.as_ref().is_some() { + lines.push(Line::from("signed in with chatgpt".bold())); + + if let Some(tokens) = auth.tokens.as_ref() { + let info = tokens.id_token.clone(); + if let Some(email) = info.email { + lines.push(Line::from(vec![" login: ".bold(), email.into()])); + } + + match auth.openai_api_key.as_deref() { + Some(key) if !key.is_empty() => { + lines.push(Line::from(" using api key")); + } + _ => { + let plan_text = info + .chatgpt_plan_type + .unwrap_or_else(|| "Unknown".to_string()); + lines.push(Line::from(vec![" plan: ".bold(), plan_text.into()])); + } + } + } + + lines.push(Line::from("")); + } + } + + // Token usage lines.push(Line::from("token usage".bold())); lines.push(Line::from(vec![ " input: ".bold(), From 13982d6b4e79415ffa435af1cb0f584807062275 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 7 Aug 2025 01:30:13 -0700 Subject: [PATCH 0072/1309] chore: fix outstanding review comments from the bot on #1919 (#1928) I should have read the comments before submitting! --- codex-rs/config.md | 2 +- codex-rs/core/src/config.rs | 6 +++--- codex-rs/core/src/config_types.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/codex-rs/config.md b/codex-rs/config.md index e044684426..68687c560c 100644 --- a/codex-rs/config.md +++ b/codex-rs/config.md @@ -276,7 +276,7 @@ sandbox_mode = "workspace-write" # Extra settings that only apply when `sandbox = "workspace-write"`. [sandbox_workspace_write] # By default, the cwd for the Codex session will be writable as well as $TMPDIR -# if set) and /tmp (if it exists). Setting the respective options to `true` +# (if set) and /tmp (if it exists). Setting the respective options to `true` # will override those defaults. exclude_tmpdir_env_var = false exclude_slash_tmp = false diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index a2e0618a7f..081306dab1 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -4,7 +4,7 @@ use crate::config_types::McpServerConfig; use crate::config_types::ReasoningEffort; use crate::config_types::ReasoningSummary; use crate::config_types::SandboxMode; -use crate::config_types::SandboxWorkplaceWrite; +use crate::config_types::SandboxWorkspaceWrite; use crate::config_types::ShellEnvironmentPolicy; use crate::config_types::ShellEnvironmentPolicyToml; use crate::config_types::Tui; @@ -282,7 +282,7 @@ pub struct ConfigToml { pub sandbox_mode: Option, /// Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`. - pub sandbox_workspace_write: Option, + pub sandbox_workspace_write: Option, /// Disable server-side response storage (sends the full conversation /// context with every request). Currently necessary for OpenAI customers @@ -361,7 +361,7 @@ impl ConfigToml { match resolved_sandbox_mode { SandboxMode::ReadOnly => SandboxPolicy::new_read_only_policy(), SandboxMode::WorkspaceWrite => match self.sandbox_workspace_write.as_ref() { - Some(SandboxWorkplaceWrite { + Some(SandboxWorkspaceWrite { writable_roots, network_access, exclude_tmpdir_env_var, diff --git a/codex-rs/core/src/config_types.rs b/codex-rs/core/src/config_types.rs index a81c20502b..d584a0494d 100644 --- a/codex-rs/core/src/config_types.rs +++ b/codex-rs/core/src/config_types.rs @@ -93,7 +93,7 @@ pub enum SandboxMode { } #[derive(Deserialize, Debug, Clone, PartialEq, Default)] -pub struct SandboxWorkplaceWrite { +pub struct SandboxWorkspaceWrite { #[serde(default)] pub writable_roots: Vec, #[serde(default)] From 20084facfe4755ef4100e4b0b4cc537e5d469b60 Mon Sep 17 00:00:00 2001 From: Ed Bayes Date: Thu, 7 Aug 2025 01:45:04 -0700 Subject: [PATCH 0073/1309] Add spinner animation to TUI status indicator (#1917) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - add a pulsing dot loader before the shimmering `Working` label in the status indicator widget and include a small test asserting the spinner character is rendered - also fix a small bug in the ran command header by adding a space between the ⚡ and `Ran command` https://github.com/user-attachments/assets/6768c9d2-e094-49cb-ad51-44bcac10aa6f ## Testing - `just fmt` - `just fix` *(failed: E0658 `let` expressions in core/src/client.rs)* - `cargo test --all-features` *(failed: E0658 `let` expressions in core/src/client.rs)* ------ https://chatgpt.com/codex/tasks/task_i_68941bffdb948322b0f4190bc9dbe7f6 --------- Co-authored-by: aibrahim-oai --- codex-rs/tui/src/history_cell.rs | 4 +-- codex-rs/tui/src/status_indicator_widget.rs | 32 ++++++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index d7a06fa576..b87525a02a 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -262,7 +262,7 @@ impl HistoryCell { let mut lines: Vec> = Vec::new(); let command_escaped = strip_bash_lc_and_escape(&command); lines.push(Line::from(vec![ - "⚡Ran command ".magenta(), + "⚡ Ran command ".magenta(), command_escaped.into(), ])); @@ -556,7 +556,7 @@ impl HistoryCell { let mut header: Vec = Vec::new(); header.push(Span::raw("📋")); header.push(Span::styled( - "Updated", + " Updated", Style::default().add_modifier(Modifier::BOLD).magenta(), )); header.push(Span::raw(" to do list [")); diff --git a/codex-rs/tui/src/status_indicator_widget.rs b/codex-rs/tui/src/status_indicator_widget.rs index fca9a23bc9..dcb8a5fdbe 100644 --- a/codex-rs/tui/src/status_indicator_widget.rs +++ b/codex-rs/tui/src/status_indicator_widget.rs @@ -213,9 +213,20 @@ impl WidgetRef for StatusIndicatorWidget { // Plain rendering: no borders or padding so the live cell is visually indistinguishable from terminal scrollback. let inner_width = area.width as usize; - // Compose a single status line like: "▌ Working (Xs • Ctrl c to interrupt) " let mut spans: Vec> = Vec::new(); spans.push(Span::styled("▌ ", Style::default().fg(Color::Cyan))); + + // Simple dim spinner to the left of the header. + let spinner_frames = ['·', '•', '●', '•']; + const SPINNER_SLOWDOWN: usize = 2; + let spinner_ch = spinner_frames[(idx / SPINNER_SLOWDOWN) % spinner_frames.len()]; + spans.push(Span::styled( + spinner_ch.to_string(), + Style::default().fg(Color::DarkGray), + )); + spans.push(Span::raw(" ")); + + // Space after header // Animated header after the left bar spans.extend(animated_spans); // Space between header and bracket block @@ -324,4 +335,23 @@ mod tests { } assert!(row.contains("Working"), "expected Working header: {row:?}"); } + + #[test] + fn spinner_is_rendered() { + let (tx_raw, _rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let mut w = StatusIndicatorWidget::new(tx); + w.restart_with_text("Hello".to_string()); + std::thread::sleep(std::time::Duration::from_millis(120)); + + let area = ratatui::layout::Rect::new(0, 0, 30, 1); + let mut buf = ratatui::buffer::Buffer::empty(area); + w.render_ref(area, &mut buf); + + let ch = buf[(2, 0)].symbol().chars().next().unwrap_or(' '); + assert!( + matches!(ch, '·' | '•' | '●'), + "expected spinner char at col 2: {ch:?}" + ); + } } From c2c327c72317455fad1378c79c3024ec95a1519b Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 7 Aug 2025 01:55:41 -0700 Subject: [PATCH 0074/1309] feat: change shell_environment_policy to default to inherit="all" (#1904) Trying to use `core` as the default has been "too clever." Users can always take responsibility for controlling the env without this setting at all by specifying the `env` they use when calling `codex` in the first place. See https://github.com/openai/codex/issues/1249. --- codex-rs/config.md | 7 +++---- codex-rs/core/src/config_types.rs | 5 +++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/codex-rs/config.md b/codex-rs/config.md index 68687c560c..848e7c0444 100644 --- a/codex-rs/config.md +++ b/codex-rs/config.md @@ -339,12 +339,11 @@ disable_response_storage = true ## shell_environment_policy -Codex spawns subprocesses (e.g. when executing a `local_shell` tool-call suggested by the assistant). By default it passes **only a minimal core subset** of your environment to those subprocesses to avoid leaking credentials. You can tune this behavior via the **`shell_environment_policy`** block in -`config.toml`: +Codex spawns subprocesses (e.g. when executing a `local_shell` tool-call suggested by the assistant). By default it now passes **your full environment** to those subprocesses. You can tune this behavior via the **`shell_environment_policy`** block in `config.toml`: ```toml [shell_environment_policy] -# inherit can be "core" (default), "all", or "none" +# inherit can be "all" (default), "core", or "none" inherit = "core" # set to true to *skip* the filter for `"*KEY*"` and `"*TOKEN*"` ignore_default_excludes = false @@ -358,7 +357,7 @@ include_only = ["PATH", "HOME"] | Field | Type | Default | Description | | ------------------------- | -------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -| `inherit` | string | `core` | Starting template for the environment:
`core` (`HOME`, `PATH`, `USER`, …), `all` (clone full parent env), or `none` (start empty). | +| `inherit` | string | `all` | Starting template for the environment:
`all` (clone full parent env), `core` (`HOME`, `PATH`, `USER`, …), or `none` (start empty). | | `ignore_default_excludes` | boolean | `false` | When `false`, Codex removes any var whose **name** contains `KEY`, `SECRET`, or `TOKEN` (case-insensitive) before other rules run. | | `exclude` | array<string> | `[]` | Case-insensitive glob patterns to drop after the default filter.
Examples: `"AWS_*"`, `"AZURE_*"`. | | `set` | table<string,string> | `{}` | Explicit key/value overrides or additions – always win over inherited values. | diff --git a/codex-rs/core/src/config_types.rs b/codex-rs/core/src/config_types.rs index d584a0494d..291dcb6422 100644 --- a/codex-rs/core/src/config_types.rs +++ b/codex-rs/core/src/config_types.rs @@ -109,10 +109,10 @@ pub struct SandboxWorkspaceWrite { pub enum ShellEnvironmentPolicyInherit { /// "Core" environment variables for the platform. On UNIX, this would /// include HOME, LOGNAME, PATH, SHELL, and USER, among others. - #[default] Core, /// Inherits the full environment from the parent process. + #[default] All, /// Do not inherit any environment variables from the parent process. @@ -171,7 +171,8 @@ pub struct ShellEnvironmentPolicy { impl From for ShellEnvironmentPolicy { fn from(toml: ShellEnvironmentPolicyToml) -> Self { - let inherit = toml.inherit.unwrap_or(ShellEnvironmentPolicyInherit::Core); + // Default to inheriting the full environment when not specified. + let inherit = toml.inherit.unwrap_or(ShellEnvironmentPolicyInherit::All); let ignore_default_excludes = toml.ignore_default_excludes.unwrap_or(false); let exclude = toml .exclude From 5589c6089bbabb52b58ec9c3014d37ca3ce95d7e Mon Sep 17 00:00:00 2001 From: aibrahim-oai Date: Thu, 7 Aug 2025 02:02:56 -0700 Subject: [PATCH 0075/1309] approval ui (#1933) Asking for approval: image Allow: image Reject: image Always Approve: image --- codex-rs/tui/src/chatwidget.rs | 12 ---- codex-rs/tui/src/history_cell.rs | 3 +- codex-rs/tui/src/user_approval_widget.rs | 72 ++++++++++++++++++------ 3 files changed, 58 insertions(+), 29 deletions(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 9936c0eef9..d4872d377e 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -45,7 +45,6 @@ use crate::bottom_pane::BottomPane; use crate::bottom_pane::BottomPaneParams; use crate::bottom_pane::CancellationEvent; use crate::bottom_pane::InputResult; -use crate::exec_command::strip_bash_lc_and_escape; use crate::history_cell::CommandOutput; use crate::history_cell::HistoryCell; use crate::history_cell::PatchEventType; @@ -393,17 +392,6 @@ impl ChatWidget<'_> { reason, }) => { self.finalize_active_stream(); - // Log a background summary immediately so the history is chronological. - let cmdline = strip_bash_lc_and_escape(&command); - let text = format!( - "command requires approval:\n$ {cmdline}{reason}", - reason = reason - .as_ref() - .map(|r| format!("\n{r}")) - .unwrap_or_default() - ); - self.add_to_history(HistoryCell::new_background_event(text)); - let request = ApprovalRequest::Exec { id, command, diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index b87525a02a..903e3087aa 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -435,7 +435,8 @@ impl HistoryCell { view: TextBlock::new(lines), } } - + // allow dead code for now. maybe we'll use it again. + #[allow(dead_code)] pub(crate) fn new_background_event(message: String) -> Self { let mut lines: Vec> = Vec::new(); lines.push(Line::from("event".dim())); diff --git a/codex-rs/tui/src/user_approval_widget.rs b/codex-rs/tui/src/user_approval_widget.rs index 70b355d794..966b8d68f9 100644 --- a/codex-rs/tui/src/user_approval_widget.rs +++ b/codex-rs/tui/src/user_approval_widget.rs @@ -28,7 +28,6 @@ use ratatui::widgets::Wrap; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; -use crate::exec_command::relativize_to_home; use crate::exec_command::strip_bash_lc_and_escape; /// Request coming from the agent that needs user approval. @@ -36,6 +35,7 @@ pub(crate) enum ApprovalRequest { Exec { id: String, command: Vec, + #[allow(dead_code)] cwd: PathBuf, reason: Option, }, @@ -115,21 +115,18 @@ impl UserApprovalWidget<'_> { pub(crate) fn new(approval_request: ApprovalRequest, app_event_tx: AppEventSender) -> Self { let confirmation_prompt = match &approval_request { ApprovalRequest::Exec { - command, - cwd, - reason, - .. + command, reason, .. } => { let cmd = strip_bash_lc_and_escape(command); - // Maybe try to relativize to the cwd of this process first? - // Will make cwd_str shorter in the common case. - let cwd_str = match relativize_to_home(cwd) { - Some(rel) => format!("~/{}", rel.display()), - None => cwd.display().to_string(), - }; + // Present a single-line summary without cwd: "codex wants to run: " + let mut cmd_span: Span = cmd.clone().into(); + cmd_span.style = cmd_span.style.add_modifier(Modifier::DIM); let mut contents: Vec = vec![ - Line::from(vec!["codex".bold().magenta(), " wants to run:".into()]), - Line::from(vec![cwd_str.dim(), "$".into(), format!(" {cmd}").into()]), + Line::from(vec![ + "? ".fg(Color::Blue), + "Codex wants to run ".bold(), + cmd_span, + ]), Line::from(""), ]; if let Some(reason) = reason { @@ -243,9 +240,52 @@ impl UserApprovalWidget<'_> { match &self.approval_request { ApprovalRequest::Exec { command, .. } => { let cmd = strip_bash_lc_and_escape(command); - lines.push(Line::from("approval decision")); - lines.push(Line::from(format!("$ {cmd}"))); - lines.push(Line::from(format!("decision: {decision:?}"))); + let mut cmd_span: Span = cmd.clone().into(); + cmd_span.style = cmd_span.style.add_modifier(Modifier::DIM); + + // Result line based on decision. + match decision { + ReviewDecision::Approved => { + lines.push(Line::from(vec![ + "✓ ".fg(Color::Green), + "You ".into(), + "approved".bold(), + " codex to run ".into(), + cmd_span, + " ".into(), + "this time".bold(), + ])); + } + ReviewDecision::ApprovedForSession => { + lines.push(Line::from(vec![ + "✓ ".fg(Color::Green), + "You ".into(), + "approved".bold(), + " codex to run ".into(), + cmd_span, + " ".into(), + "every time this session".bold(), + ])); + } + ReviewDecision::Denied => { + lines.push(Line::from(vec![ + "✗ ".fg(Color::Red), + "You ".into(), + "did not approve".bold(), + " codex to run ".into(), + cmd_span, + ])); + } + ReviewDecision::Abort => { + lines.push(Line::from(vec![ + "✗ ".fg(Color::Red), + "You ".into(), + "canceled".bold(), + " the request to run ".into(), + cmd_span, + ])); + } + } } ApprovalRequest::ApplyPatch { .. } => { lines.push(Line::from(format!("patch approval decision: {decision:?}"))); From 1e4bf816531b233af0880c5492c30b951720321f Mon Sep 17 00:00:00 2001 From: Ed Bayes Date: Thu, 7 Aug 2025 03:29:33 -0700 Subject: [PATCH 0076/1309] Update copy (#1935) Updated copy --------- Co-authored-by: pap-openai --- .../components/chat/terminal-chat-input.tsx | 2 +- codex-cli/src/components/help-overlay.tsx | 2 +- .../src/utils/get-api-key-components.tsx | 2 +- codex-rs/tui/src/bottom_pane/chat_composer.rs | 2 +- ...tom_pane__chat_composer__tests__empty.snap | 2 +- codex-rs/tui/src/history_cell.rs | 9 ++-- codex-rs/tui/src/onboarding/auth.rs | 52 +++++++++++++------ codex-rs/tui/src/onboarding/welcome.rs | 4 +- codex-rs/tui/src/slash_command.rs | 16 +++--- codex-rs/tui/src/status_indicator_widget.rs | 2 +- 10 files changed, 57 insertions(+), 36 deletions(-) diff --git a/codex-cli/src/components/chat/terminal-chat-input.tsx b/codex-cli/src/components/chat/terminal-chat-input.tsx index c8c5bf8216..66428f8463 100644 --- a/codex-cli/src/components/chat/terminal-chat-input.tsx +++ b/codex-cli/src/components/chat/terminal-chat-input.tsx @@ -854,7 +854,7 @@ export default function TerminalChatInput({ /> ) : ( - ctrl+c to exit | "/" to see commands | enter to send + Ctrl+C to exit | "/" to see commands | Enter to send {contextLeftPercent > 25 && ( <> {" — "} diff --git a/codex-cli/src/components/help-overlay.tsx b/codex-cli/src/components/help-overlay.tsx index d302f7551e..1c24ad9c72 100644 --- a/codex-cli/src/components/help-overlay.tsx +++ b/codex-cli/src/components/help-overlay.tsx @@ -96,7 +96,7 @@ export default function HelpOverlay({ - esc or q to close + Esc or q to close ); diff --git a/codex-cli/src/utils/get-api-key-components.tsx b/codex-cli/src/utils/get-api-key-components.tsx index 45346632c4..d23b661126 100644 --- a/codex-cli/src/utils/get-api-key-components.tsx +++ b/codex-cli/src/utils/get-api-key-components.tsx @@ -68,7 +68,7 @@ export function WaitingForAuth(): JSX.Element { {" "} - Waiting for authentication… ctrl + c to quit + Waiting for authentication… Ctrl + C to quit ); diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index e53fe03676..3be3f14d6f 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -31,7 +31,7 @@ use crate::bottom_pane::textarea::TextAreaState; use codex_file_search::FileMatch; use std::cell::RefCell; -const BASE_PLACEHOLDER_TEXT: &str = "..."; +const BASE_PLACEHOLDER_TEXT: &str = "Ask Codex to do anything"; /// If the pasted content exceeds this number of characters, replace it with a /// placeholder in the UI. const LARGE_PASTE_CHAR_THRESHOLD: usize = 1000; diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap index 7a1a7f4ef3..de227a3071 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap @@ -2,7 +2,7 @@ source: tui/src/bottom_pane/chat_composer.rs expression: terminal.backend() --- -"▌ ... " +"▌ Ask Codex to do anything " "▌ " "▌ " "▌ " diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 903e3087aa..d0be52a472 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -196,12 +196,11 @@ impl HistoryCell { Span::raw(format!(" {cwd_str}")).dim(), ]), Line::from("".dim()), - Line::from(" Try one of the following commands to get started:".dim()), + Line::from(" To get started, describe a task or try one of these commands:".dim()), Line::from("".dim()), - Line::from(format!(" 1. /init - {}", SlashCommand::Init.description()).dim()), - Line::from(format!(" 2. /status - {}", SlashCommand::Status.description()).dim()), - Line::from(format!(" 3. /compact - {}", SlashCommand::Compact.description()).dim()), - Line::from(format!(" 4. /new - {}", SlashCommand::New.description()).dim()), + Line::from(format!(" /init - {}", SlashCommand::Init.description()).dim()), + Line::from(format!(" /status - {}", SlashCommand::Status.description()).dim()), + Line::from(format!(" /diff - {}", SlashCommand::Diff.description()).dim()), Line::from("".dim()), ]; HistoryCell::WelcomeMessage { diff --git a/codex-rs/tui/src/onboarding/auth.rs b/codex-rs/tui/src/onboarding/auth.rs index b91bf4a0d9..397ba76890 100644 --- a/codex-rs/tui/src/onboarding/auth.rs +++ b/codex-rs/tui/src/onboarding/auth.rs @@ -104,7 +104,14 @@ impl AuthModeWidget { Line::from(vec![ Span::raw("> "), Span::styled( - "Sign in with your ChatGPT account?", + "Sign in with ChatGPT to use Codex as part of your paid plan", + Style::default().add_modifier(Modifier::BOLD), + ), + ]), + Line::from(vec![ + Span::raw(" "), + Span::styled( + "or connect an API key for usage-based billing", Style::default().add_modifier(Modifier::BOLD), ), ]), @@ -145,18 +152,18 @@ impl AuthModeWidget { lines.extend(create_mode_item( 0, AuthMode::ChatGPT, - "Sign in with ChatGPT or create a new account", - "Leverages your plan, starting at $20 a month for Plus", + "Sign in with ChatGPT", + "Usage included with Plus, Pro, and Team plans", )); lines.extend(create_mode_item( 1, AuthMode::ApiKey, "Provide your own API key", - "Pay only for what you use", + "Pay for what you use", )); lines.push(Line::from("")); lines.push( - Line::from("Press Enter to continue") + Line::from(" Press Enter to continue") .style(Style::default().add_modifier(Modifier::DIM)), ); if let Some(err) = &self.error { @@ -179,8 +186,7 @@ impl AuthModeWidget { let lines = vec![ Line::from(spans), Line::from(""), - Line::from(" Press Escape to cancel") - .style(Style::default().add_modifier(Modifier::DIM)), + Line::from(" Press Esc to cancel").style(Style::default().add_modifier(Modifier::DIM)), ]; Paragraph::new(lines) .wrap(Wrap { trim: false }) @@ -194,17 +200,30 @@ impl AuthModeWidget { Line::from(""), Line::from("> Before you start:"), Line::from(""), - Line::from(" Codex can make mistakes"), - Line::from(" Check important info") - .style(Style::default().add_modifier(Modifier::DIM)), + Line::from(" Decide how much autonomy you want to grant Codex"), + Line::from(vec![ + Span::raw(" For more details see the "), + Span::styled( + "\u{1b}]8;;https://github.com/openai/codex\u{7}Codex docs\u{1b}]8;;\u{7}", + Style::default().add_modifier(Modifier::UNDERLINED), + ), + ]) + .style(Style::default().add_modifier(Modifier::DIM)), Line::from(""), - Line::from(" Due to prompt injection risks, only use it with code you trust"), - Line::from(" For more details see https://github.com/openai/codex") + Line::from(" Codex can make mistakes") + .style(Style::default().fg(Color::White)), + Line::from(" Review the code it writes and commands it runs") .style(Style::default().add_modifier(Modifier::DIM)), Line::from(""), Line::from(" Powered by your ChatGPT account"), - Line::from(" Uses your plan's rate limits and training data preferences") - .style(Style::default().add_modifier(Modifier::DIM)), + Line::from(vec![ + Span::raw(" Uses your plan's rate limits and "), + Span::styled( + "\u{1b}]8;;https://chatgpt.com/#settings\u{7}training data preferences\u{1b}]8;;\u{7}", + Style::default().add_modifier(Modifier::UNDERLINED), + ), + ]) + .style(Style::default().add_modifier(Modifier::DIM)), Line::from(""), Line::from(" Press Enter to continue").style(Style::default().fg(LIGHT_BLUE)), ]; @@ -236,7 +255,10 @@ impl AuthModeWidget { fn render_env_var_missing(&self, area: Rect, buf: &mut Buffer) { let lines = vec![ - Line::from("✘ OPENAI_API_KEY not found").style(Style::default().fg(Color::Red)), + Line::from( + " To use Codex with the OpenAI API, set OPENAI_API_KEY in your environment", + ) + .style(Style::default().fg(Color::Blue)), Line::from(""), Line::from(" Press Enter to return") .style(Style::default().add_modifier(Modifier::DIM)), diff --git a/codex-rs/tui/src/onboarding/welcome.rs b/codex-rs/tui/src/onboarding/welcome.rs index a35f6528ab..bcdc5c9aae 100644 --- a/codex-rs/tui/src/onboarding/welcome.rs +++ b/codex-rs/tui/src/onboarding/welcome.rs @@ -18,9 +18,9 @@ pub(crate) struct WelcomeWidget { impl WidgetRef for &WelcomeWidget { fn render_ref(&self, area: Rect, buf: &mut Buffer) { let line = Line::from(vec![ - Span::raw("> "), + Span::raw(">_ "), Span::styled( - "Welcome to Codex, OpenAI's coding agent that runs in your terminal", + "Welcome to Codex, OpenAI's command-line coding agent", Style::default().add_modifier(Modifier::BOLD), ), ]); diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index 0513d64443..ba24ead4f9 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -27,15 +27,15 @@ impl SlashCommand { /// User-visible description shown in the popup. pub fn description(self) -> &'static str { match self { - SlashCommand::New => "Start a new chat", - SlashCommand::Init => "Create an AGENTS.md file with instructions for Codex", - SlashCommand::Compact => "Compact the chat history", - SlashCommand::Quit => "Exit the application", - SlashCommand::Diff => "Show git diff (including untracked files)", - SlashCommand::Status => "Show current session configuration and token usage", - SlashCommand::Logout => "Log out of Codex", + SlashCommand::New => "start a new chat during a conversation", + SlashCommand::Init => "create an AGENTS.md file with instructions for Codex", + SlashCommand::Compact => "summarize conversation to prevent hitting the context limit", + SlashCommand::Quit => "exit Codex", + SlashCommand::Diff => "show git diff (including untracked files)", + SlashCommand::Status => "show current session configuration and token usage", + SlashCommand::Logout => "log out of Codex", #[cfg(debug_assertions)] - SlashCommand::TestApproval => "Test approval request", + SlashCommand::TestApproval => "test approval request", } } diff --git a/codex-rs/tui/src/status_indicator_widget.rs b/codex-rs/tui/src/status_indicator_widget.rs index dcb8a5fdbe..513422bb4c 100644 --- a/codex-rs/tui/src/status_indicator_widget.rs +++ b/codex-rs/tui/src/status_indicator_widget.rs @@ -238,7 +238,7 @@ impl WidgetRef for StatusIndicatorWidget { Style::default().fg(Color::Gray).add_modifier(Modifier::DIM), )); spans.push(Span::styled( - "Ctrl c", + "Ctrl C", Style::default() .fg(Color::Gray) .add_modifier(Modifier::DIM | Modifier::BOLD), From 7c20160676d6fd584a01e09d22d371e106e4678f Mon Sep 17 00:00:00 2001 From: ae Date: Thu, 7 Aug 2025 03:55:59 -0700 Subject: [PATCH 0077/1309] feat: /prompts slash command (#1937) - Shows several example prompts which include @-mentions ------ https://chatgpt.com/codex/tasks/task_i_6894779ba8cc832ca0c871d17ee06aae --- codex-rs/tui/src/app.rs | 5 +++++ codex-rs/tui/src/chatwidget.rs | 4 ++++ codex-rs/tui/src/history_cell.rs | 22 ++++++++++++++++++++++ codex-rs/tui/src/slash_command.rs | 2 ++ 4 files changed, 33 insertions(+) diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 1ba8883b0b..0e38aba3d7 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -360,6 +360,11 @@ impl App<'_> { widget.add_status_output(); } } + SlashCommand::Prompts => { + if let AppState::Chat { widget } = &mut self.app_state { + widget.add_prompts_output(); + } + } #[cfg(debug_assertions)] SlashCommand::TestApproval => { use std::collections::HashMap; diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index d4872d377e..2cadc13c49 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -556,6 +556,10 @@ impl ChatWidget<'_> { )); } + pub(crate) fn add_prompts_output(&mut self) { + self.add_to_history(HistoryCell::new_prompts_output()); + } + /// Forward file-search results to the bottom pane. pub(crate) fn apply_file_search_result(&mut self, query: String, matches: Vec) { self.bottom_pane.on_file_search_result(query, matches); diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index d0be52a472..bbf1d7210c 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -110,6 +110,9 @@ pub(crate) enum HistoryCell { /// Output from the `/status` command. StatusOutput { view: TextBlock }, + /// Output from the `/prompts` command. + PromptsOutput { view: TextBlock }, + /// Error event from the backend. ErrorEvent { view: TextBlock }, @@ -142,6 +145,7 @@ impl HistoryCell { | HistoryCell::BackgroundEvent { view } | HistoryCell::GitDiffOutput { view } | HistoryCell::StatusOutput { view } + | HistoryCell::PromptsOutput { view } | HistoryCell::ErrorEvent { view } | HistoryCell::SessionInfo { view } | HistoryCell::CompletedExecCommand { view } @@ -201,6 +205,7 @@ impl HistoryCell { Line::from(format!(" /init - {}", SlashCommand::Init.description()).dim()), Line::from(format!(" /status - {}", SlashCommand::Status.description()).dim()), Line::from(format!(" /diff - {}", SlashCommand::Diff.description()).dim()), + Line::from(format!(" /prompts - {}", SlashCommand::Prompts.description()).dim()), Line::from("".dim()), ]; HistoryCell::WelcomeMessage { @@ -525,6 +530,23 @@ impl HistoryCell { } } + pub(crate) fn new_prompts_output() -> Self { + let lines: Vec> = vec![ + Line::from("/prompts".magenta()), + Line::from(""), + Line::from(" 1. Explain this codebase"), + Line::from(" 2. Summarize recent commits"), + Line::from(" 3. Implement {feature}"), + Line::from(" 4. Find and fix a bug in @filename"), + Line::from(" 5. Write tests for @filename"), + Line::from(" 6. Improve documentation in @filename"), + Line::from(""), + ]; + HistoryCell::PromptsOutput { + view: TextBlock::new(lines), + } + } + pub(crate) fn new_error_event(message: String) -> Self { let lines: Vec> = vec![vec!["🖐 ".red().bold(), message.into()].into(), "".into()]; diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index ba24ead4f9..e58ab8521e 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -17,6 +17,7 @@ pub enum SlashCommand { Compact, Diff, Status, + Prompts, Logout, Quit, #[cfg(debug_assertions)] @@ -33,6 +34,7 @@ impl SlashCommand { SlashCommand::Quit => "exit Codex", SlashCommand::Diff => "show git diff (including untracked files)", SlashCommand::Status => "show current session configuration and token usage", + SlashCommand::Prompts => "show example prompts", SlashCommand::Logout => "log out of Codex", #[cfg(debug_assertions)] SlashCommand::TestApproval => "test approval request", From c4dc6a80bf349641a0270cb2ac78668ebade1849 Mon Sep 17 00:00:00 2001 From: ae Date: Thu, 7 Aug 2025 04:02:58 -0700 Subject: [PATCH 0078/1309] feat: improve output of /status (#1936) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now it looks like this: ``` /status 📂 Workspace • Path: ~/code/codex/codex-rs • Approval Mode: on-request • Sandbox: workspace-write 👤 Account • Signed in with ChatGPT • Login: example@example.com • Plan: Pro 🧠 Model • Name: ?!?!?!?!?! • Provider: OpenAI 📊 Token Usage • Input: 11940 (+ 7999 cached) • Output: 2639 • Total: 14579 ``` --- codex-rs/tui/src/history_cell.rs | 148 ++++++++++++++++++++++++------- 1 file changed, 118 insertions(+), 30 deletions(-) diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index bbf1d7210c..5d2ea35c4f 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -13,6 +13,7 @@ use codex_core::plan_tool::StepStatus; use codex_core::plan_tool::UpdatePlanArgs; use codex_core::protocol::FileChange; use codex_core::protocol::McpInvocation; +use codex_core::protocol::SandboxPolicy; use codex_core::protocol::SessionConfiguredEvent; use codex_core::protocol::TokenUsage; use codex_login::get_auth_file; @@ -134,6 +135,27 @@ pub(crate) enum HistoryCell { const TOOL_CALL_MAX_LINES: usize = 3; +fn title_case(s: &str) -> String { + if s.is_empty() { + return String::new(); + } + let mut chars = s.chars(); + let first = match chars.next() { + Some(c) => c, + None => return String::new(), + }; + let rest: String = chars.as_str().to_ascii_lowercase(); + first.to_uppercase().collect::() + &rest +} + +fn pretty_provider_name(id: &str) -> String { + if id.eq_ignore_ascii_case("openai") { + "OpenAI".to_string() + } else { + title_case(id) + } +} + impl HistoryCell { /// Return a cloned, plain representation of the cell's lines suitable for /// one‑shot insertion into the terminal scrollback. Image cells are @@ -471,35 +493,65 @@ impl HistoryCell { let mut lines: Vec> = Vec::new(); lines.push(Line::from("/status".magenta())); - // Config - for (key, value) in create_config_summary_entries(config) { - lines.push(Line::from(vec![format!("{key}: ").bold(), value.into()])); - } + let config_entries = create_config_summary_entries(config); + let lookup = |k: &str| -> String { + config_entries + .iter() + .find(|(key, _)| *key == k) + .map(|(_, v)| v.clone()) + .unwrap_or_default() + }; + + // 📂 Workspace + lines.push(Line::from(vec!["📂 ".into(), "Workspace".bold()])); + // Path (home-relative, e.g., ~/code/project) + let cwd_str = match relativize_to_home(&config.cwd) { + Some(rel) if !rel.as_os_str().is_empty() => format!("~/{}", rel.display()), + Some(_) => "~".to_string(), + None => config.cwd.display().to_string(), + }; + lines.push(Line::from(vec![" • Path: ".into(), cwd_str.into()])); + // Approval mode (as-is) + lines.push(Line::from(vec![ + " • Approval Mode: ".into(), + lookup("approval").into(), + ])); + // Sandbox (simplified name only) + let sandbox_name = match &config.sandbox_policy { + SandboxPolicy::DangerFullAccess => "danger-full-access", + SandboxPolicy::ReadOnly => "read-only", + SandboxPolicy::WorkspaceWrite { .. } => "workspace-write", + }; + lines.push(Line::from(vec![ + " • Sandbox: ".into(), + sandbox_name.into(), + ])); lines.push(Line::from("")); - // Auth + // 👤 Account (only if ChatGPT tokens exist), shown under the first block let auth_file = get_auth_file(&config.codex_home); if let Ok(auth) = try_read_auth_json(&auth_file) { - if auth.tokens.as_ref().is_some() { - lines.push(Line::from("signed in with chatgpt".bold())); + if let Some(tokens) = auth.tokens.clone() { + lines.push(Line::from(vec!["👤 ".into(), "Account".bold()])); + lines.push(Line::from(" • Signed in with ChatGPT")); - if let Some(tokens) = auth.tokens.as_ref() { - let info = tokens.id_token.clone(); - if let Some(email) = info.email { - lines.push(Line::from(vec![" login: ".bold(), email.into()])); + let info = tokens.id_token; + if let Some(email) = info.email { + lines.push(Line::from(vec![" • Login: ".into(), email.into()])); + } + + match auth.openai_api_key.as_deref() { + Some(key) if !key.is_empty() => { + lines.push(Line::from(" • Using API key")); } - - match auth.openai_api_key.as_deref() { - Some(key) if !key.is_empty() => { - lines.push(Line::from(" using api key")); - } - _ => { - let plan_text = info - .chatgpt_plan_type - .unwrap_or_else(|| "Unknown".to_string()); - lines.push(Line::from(vec![" plan: ".bold(), plan_text.into()])); - } + _ => { + let plan_text = info + .chatgpt_plan_type + .as_deref() + .map(title_case) + .unwrap_or_else(|| "Unknown".to_string()); + lines.push(Line::from(vec![" • Plan: ".into(), plan_text.into()])); } } @@ -507,20 +559,56 @@ impl HistoryCell { } } - // Token usage - lines.push(Line::from("token usage".bold())); + // 🧠 Model + lines.push(Line::from(vec!["🧠 ".into(), "Model".bold()])); lines.push(Line::from(vec![ - " input: ".bold(), - usage.non_cached_input().to_string().into(), - " ".into(), - format!("(+ {} cached)", usage.cached_input()).into(), + " • Name: ".into(), + config.model.clone().into(), ])); + let provider_disp = pretty_provider_name(&config.model_provider_id); lines.push(Line::from(vec![ - " output: ".bold(), + " • Provider: ".into(), + provider_disp.into(), + ])); + // Only show Reasoning fields if present in config summary + let reff = lookup("reasoning effort"); + if !reff.is_empty() { + lines.push(Line::from(vec![ + " • Reasoning Effort: ".into(), + title_case(&reff).into(), + ])); + } + let rsum = lookup("reasoning summaries"); + if !rsum.is_empty() { + lines.push(Line::from(vec![ + " • Reasoning Summaries: ".into(), + title_case(&rsum).into(), + ])); + } + + lines.push(Line::from("")); + + // 📊 Token Usage + lines.push(Line::from(vec!["📊 ".into(), "Token Usage".bold()])); + // Input: [+ cached] + let mut input_line_spans: Vec> = vec![ + " • Input: ".into(), + usage.non_cached_input().to_string().into(), + ]; + if let Some(cached) = usage.cached_input_tokens { + if cached > 0 { + input_line_spans.push(format!(" (+ {cached} cached)").into()); + } + } + lines.push(Line::from(input_line_spans)); + // Output: + lines.push(Line::from(vec![ + " • Output: ".into(), usage.output_tokens.to_string().into(), ])); + // Total: lines.push(Line::from(vec![ - " total: ".bold(), + " • Total: ".into(), usage.blended_total().to_string().into(), ])); From 12d29c277942b75285347b7a214895f9177aeec0 Mon Sep 17 00:00:00 2001 From: ae Date: Thu, 7 Aug 2025 04:10:13 -0700 Subject: [PATCH 0079/1309] feat: add tip to upgrade to ChatGPT plan (#1938) --- codex-rs/tui/src/history_cell.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 5d2ea35c4f..61ab01e965 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -543,7 +543,9 @@ impl HistoryCell { match auth.openai_api_key.as_deref() { Some(key) if !key.is_empty() => { - lines.push(Line::from(" • Using API key")); + lines.push(Line::from( + " • Using API key. Run codex login to use ChatGPT plan", + )); } _ => { let plan_text = info From 81b148bda271615b37f7e04b3135e9d552df8111 Mon Sep 17 00:00:00 2001 From: ae Date: Thu, 7 Aug 2025 04:29:50 -0700 Subject: [PATCH 0080/1309] feat: update system prompt (#1939) --- codex-rs/core/prompt.md | 346 +++++++++++++++++++++++++++++++--------- 1 file changed, 268 insertions(+), 78 deletions(-) diff --git a/codex-rs/core/prompt.md b/codex-rs/core/prompt.md index d5d96a89b4..4711dd749a 100644 --- a/codex-rs/core/prompt.md +++ b/codex-rs/core/prompt.md @@ -1,69 +1,273 @@ -You are operating as and within the Codex CLI, an open-source, terminal-based agentic coding assistant built by OpenAI. It wraps OpenAI models to enable natural language interaction with a local codebase. You are expected to be precise, safe, and helpful. +You are a coding agent running in the Codex CLI, a terminal-based coding assistant. Codex CLI is an open source project led by OpenAI. You are expected to be precise, safe, and helpful. Your capabilities: -- Receive user prompts, project context, and files. -- Stream responses and emit function calls (e.g., shell commands, code edits). -- Run commands, like apply_patch, and manage user approvals based on policy. -- Work inside a workspace with sandboxing instructions specified by the policy described in (## Sandbox environment and approval instructions) +- Receive user prompts and other context provided by the harness, such as files in the workspace. +- Communicate with the user by streaming thinking & responses, and by making & updating plans. +- Emit function calls to run terminal commands and apply patches. Depending on how this specific run is configured, you can request that these function calls be escalated to the user for approval before running. More on this in the "Sandbox and approvals" section. Within this context, Codex refers to the open-source agentic coding interface (not the old Codex language model built by OpenAI). -## General guidelines -As a deployed coding agent, please continue working on the user's task until their query is resolved, before ending your turn and yielding back to the user. Only terminate your turn when you are sure that the task is solved. If you are not sure about file content or codebase structure pertaining to the user's request, use your tools to read files and gather the relevant information. Do NOT guess or make up an answer. +# How you work -After a user sends their first message, you should immediately provide a brief message acknowledging their request to set the tone and expectation of future work to be done (no more than 8-10 words). This should be done before performing work like exploring the codebase, writing or reading files, or other tool calls needed to complete the task. Use a natural, collaborative tone similar to how a teammate would receive a task during a pair programming session. +## Personality -Please resolve the user's task by editing the code files in your current code execution session. Your session allows for you to modify and run code. The repo(s) are already cloned in your working directory, and you must fully solve the problem for your answer to be considered correct. +Your default personality and tone is concise, direct, and friendly. You communicate efficiently, always keeping the user clearly informed about ongoing actions without unnecessary detail. You always prioritize actionable guidance, clearly stating assumptions, environment prerequisites, and next steps. Unless explicitly asked, you avoid excessively verbose explanations about your work. -### Task execution -You MUST adhere to the following criteria when executing the task: +## Responsiveness +### Preamble messages + +Before making tool calls, send a brief preamble to the user explaining what you’re about to do. When sending preamble messages, follow these principles and examples: + +- **Logically group related actions**: if you’re about to run several related commands, describe them together in one preamble rather than sending a separate note for each. +- **Keep it concise**: be no more than 1-2 sentences (8–12 words for quick updates). +- **Build on prior context**: if this is not your first tool call, use the preamble message to connect the dots with what’s been done so far and create a sense of momentum and clarity for the user to understand your next actions. +- **Keep your tone light, friendly and curious**: add small touches of personality in preambles feel collaborative and engaging. + +**Examples:** +- “I’ve explored the repo; now checking the API route definitions.” +- “Next, I’ll patch the config and update the related tests.” +- “I’m about to scaffold the CLI commands and helper functions.” +- “Ok cool, so I’ve wrapped my head around the repo. Now digging into the API routes.” +- “Config’s looking tidy. Next up is patching helpers to keep things in sync.” +- “Finished poking at the DB gateway. I will now chase down error handling.” +- “Alright, build pipeline order is interesting. Checking how it reports failures.” +- “Spotted a clever caching util; now hunting where it gets used.” + +**Avoiding a preamble for every trivial read (e.g., `cat` a single file) unless it’s part of a larger grouped action. +- Jumping straight into tool calls without explaining what’s about to happen. +- Writing overly long or speculative preambles — focus on immediate, tangible next steps. + +## Planning + +You have access to an `update_plan` tool which tracks steps and progress and renders them to the user. Using the tool helps demonstrate that you've understood the task and convey how you're approaching it. Plans can help to make complex, ambiguous, or multi-phase work clearer and more collaborative for the user. A good plan should break the task into meaningful, logically ordered steps that are easy to verify as you go. Note that plans are not for padding out simple work with filler steps or stating the obvious. Do not repeat the full contents of the plan after an `update_plan` call — the harness already displays it. Instead, summarize the change made and highlight any important context or next step. + +Use a plan when: +- The task is non-trivial and will require multiple actions over a long time horizon. +- There are logical phases or dependencies where sequencing matters. +- The work has ambiguity that benefits from outlining high-level goals. +- You want intermediate checkpoints for feedback and validation. +- When the user asked you to do more than one thing in a single prompt +- The user has asked you to use the plan tool (aka "TODOs") +- You generate additional steps while working, and plan to do them before yielding to the user + +Skip a plan when: +- The task is simple and direct. +- Breaking it down would only produce literal or trivial steps. + +Planning steps are called "steps" in the tool, but really they're more like tasks or TODOs. As such they should be very concise descriptions of non-obvious work that an engineer might do like "Write the API spec", then "Update the backend", then "Implement the frontend". On the other hand, it's obvious that you'll usually have to "Explore the codebase" or "Implement the changes", so those are not worth tracking in your plan. + +It may be the case that you complete all steps in your plan after a single pass of implementation. If this is the case, you can simply mark all the planned steps as completed. The content of your plan should not involve doing anything that you aren't capable of doing (i.e. don't try to test things that you can't test). Do not use plans for simple or single-step queries that you can just do or answer immediately. + +### Examples + +**High-quality plans** + +Example 1: + +1. Add CLI entry with file args +2. Parse Markdown via CommonMark library +3. Apply semantic HTML template +4. Handle code blocks, images, links +5. Add error handling for invalid files + +Example 2: + +1. Define CSS variables for colors +2. Add toggle with localStorage state +3. Refactor components to use variables +4. Verify all views for readability +5. Add smooth theme-change transition + +Example 3: + +1. Set up Node.js + WebSocket server +2. Add join/leave broadcast events +3. Implement messaging with timestamps +4. Add usernames + mention highlighting +5. Persist messages in lightweight DB +6. Add typing indicators + unread count + +**Low-quality plans** + +Example 1: + +1. Create CLI tool +2. Add Markdown parser +3. Convert to HTML + +Example 2: + +1. Add dark mode toggle +2. Save preference +3. Make styles look good + +Example 3: + +1. Create single-file HTML game +2. Run quick sanity check +3. Summarize usage instructions + +If you need to write a plan, only write high quality plans, not low quality ones. + +## Task execution + +You are a coding agent. Please keep going until the query is completely resolved, before ending your turn and yielding back to the user. Only terminate your turn when you are sure that the problem is solved. Autonomously resolve the query to the best of your ability, using the tools available to you, before coming back to the user. Do NOT guess or make up an answer. + +You MUST adhere to the following criteria when solving queries: - Working on the repo(s) in the current environment is allowed, even if they are proprietary. - Analyzing code for vulnerabilities is allowed. - Showing user code and tool call details is allowed. -- User instructions may overwrite the _CODING GUIDELINES_ section in this developer message. -- `user_instructions` are not part of the user's request, but guidance for how to complete the task. -- Do not cite `user_instructions` back to the user unless a specific piece is relevant. -- Do not use \`ls -R\`, \`find\`, or \`grep\` - these are slow in large repos. Use \`rg\` and \`rg --files\`. -- Use the \`apply_patch\` shell command to edit files: {"command":["apply_patch","*** Begin Patch\\n*** Update File: path/to/file.py\\n@@ def example():\\n- pass\\n+ return 123\\n*** End Patch"]} -- If completing the user's task requires writing or modifying files: - - Your code and final answer should follow these _CODING GUIDELINES_: - - Fix the problem at the root cause rather than applying surface-level patches, when possible. - - Avoid unneeded complexity in your solution. - - Ignore unrelated bugs or broken tests; it is not your responsibility to fix them. - - Update documentation as necessary. - - Keep changes consistent with the style of the existing codebase. Changes should be minimal and focused on the task. - - Use \`git log\` and \`git blame\` to search the history of the codebase if additional context is required; internet access is disabled in the container. - - NEVER add copyright or license headers unless specifically requested. - - You do not need to \`git commit\` your changes; this will be done automatically for you. - - If there is a .pre-commit-config.yaml, use \`pre-commit run --files ...\` to check that your changes pass the pre- commit checks. However, do not fix pre-existing errors on lines you didn't touch. - - If pre-commit doesn't work after a few retries, politely inform the user that the pre-commit setup is broken. - - Once you finish coding, you must - - Check \`git status\` to sanity check your changes; revert any scratch files or changes. - - Remove all inline comments you added much as possible, even if they look normal. Check using \`git diff\`. Inline comments must be generally avoided, unless active maintainers of the repo, after long careful study of the code and the issue, will still misinterpret the code without the comments. - - Check if you accidentally add copyright or license headers. If so, remove them. - - Try to run pre-commit if it is available. - - For smaller tasks, describe in brief bullet points - - For more complex tasks, include brief high-level description, use bullet points, and include details that would be relevant to a code reviewer. -- If completing the user's task DOES NOT require writing or modifying files (e.g., the user asks a question about the code base): - - Respond in a friendly tune as a remote teammate, who is knowledgeable, capable and eager to help with coding. -- When your task involves writing or modifying files: - - Do NOT tell the user to "save the file" or "copy the code into a file" if you already created or modified the file using the `apply_patch` shell command. Instead, reference the file as already saved. - - Do NOT show the full contents of large files you have already written, unless the user explicitly asks for them. +- Use the `apply_patch` tool to edit files (NEVER try `applypatch` or `apply-patch`, only `apply_patch`): {"command":["apply_patch","*** Begin Patch\\n*** Update File: path/to/file.py\\n@@ def example():\\n- pass\\n+ return 123\\n*** End Patch"]} -## Using the shell command `apply_patch` to edit files -`apply_patch` is a shell command for editing files. Your patch language is a stripped‑down, file‑oriented diff format designed to be easy to parse and safe to apply. You can think of it as a high‑level envelope: +If completing the user's task requires writing or modifying files, your code and final answer should follow these coding guidelines, though user instructions (i.e. AGENTS.md) may override these guidelines: -*** Begin Patch +- Fix the problem at the root cause rather than applying surface-level patches, when possible. +- Avoid unneeded complexity in your solution. +- Do not attempt to fix unrelated bugs or broken tests. It is not your responsibility to fix them. (You may mention them to the user in your final message though.) +- Update documentation as necessary. +- Keep changes consistent with the style of the existing codebase. Changes should be minimal and focused on the task. +- Use `git log` and `git blame` to search the history of the codebase if additional context is required. +- NEVER add copyright or license headers unless specifically requested. +- Do not waste tokens by re-reading files after calling `apply_patch` on them. The tool call will fail if it didn't work. The same goes for making folders, deleting folders, etc. +- Do not `git commit` your changes or create new git branches unless explicitly requested. +- Do not add inline comments within code unless explicitly requested. +- Do not use one-letter variable names unless explicitly requested. +- NEVER output inline citations like "【F:README.md†L5-L14】" in your outputs. The CLI is not able to render these so they will just be broken in the UI. Instead, if you output valid filepaths, users will be able to click on them to open the files in their editor. + +## Testing your work + +If the codebase has tests or the ability to build or run, you should use them to verify that your work is complete. Generally, your testing philosophy should be to start as specific as possible to the code you changed so that you can catch issues efficiently, then make your way to broader tests as you build confidence. If there's no test for the code you changed, and if the adjacent patterns in the codebases show that there's a logical place for you to add a test, you may do so. However, do not add tests to codebases with no tests, or where the patterns don't indicate so. + +Once you're confident in correctness, use formatting commands to ensure that your code is well formatted. These commands can take time so you should run them on as precise a target as possible. If there are issues you can iterate up to 3 times to get formatting right, but if you still can't manage it's better to save the user time and present them a correct solution where you call out the formatting in your final message. If the codebase does not have a formatter configured, do not add one. + +For all of testing, running, building, and formatting, do not attempt to fix unrelated bugs. It is not your responsibility to fix them. (You may mention them to the user in your final message though.) + +## Sandbox and approvals + +The Codex CLI harness supports several different sandboxing, and approval configurations that the user can choose from. + +Filesystem sandboxing prevents you from editing files without user approval. The options are: +- *read-only*: You can only read files. +- *workspace-write*: You can read files. You can write to files in your workspace folder, but not outside it. +- *danger-full-access*: No filesystem sandboxing. + +Network sandboxing prevents you from accessing network without approval. Options are +- *ON* +- *OFF* + +Approvals are your mechanism to get user consent to perform more privileged actions. Although they introduce friction to the user because your work is paused until the user responds, you should leverage them to accomplish your important work. Do not let these settings or the sandbox deter you from attempting to accomplish the user's task. Approval options are +- *untrusted*: The harness will escalate most commands for user approval, apart from a limited allowlist of safe "read" commands. +- *on-failure*: The harness will allow all commands to run in the sandbox (if enabled), and failures will be escalated to the user for approval to run again without the sandbox. +- *on-request*: Commands will be run in the sandbox by default, and you can specify in your tool call if you want to escalate a command to run without sandboxing. (Note that this mode is not always available. If it is, you'll see parameters for it in the `shell` command description.) +- *never*: This is a non-interactive mode where you may NEVER ask the user for approval to run commands. Instead, you must always persist and work around constraints to solve the task for the user. You MUST do your utmost best to finish the task and validate your work before yielding. If this mode is pared with `danger-full-access`, take advantage of it to deliver the best outcome for the user. Further, in this mode, your default testing philosophy is overridden: Even if you don't see local patterns for testing, you may add tests and scripts to validate your work. Just remove them before yielding. + +When you are running with approvals `on-request`, and sandboxing enabled, here are scenarios where you'll need to request approval: +- You need to run a command that writes to a directory that requires it (e.g. running tests that write to /tmp) +- You need to run a GUI app (e.g., open/xdg-open/osascript) to open browsers or files. +- You are running sandboxed and need to run a command that requires network access (e.g. installing packages) +- If you run a command that is important to solving the user's query, but it fails because of sandboxing, rerun the command with approval. +- You are about to take a potentially destructive action such as an `rm` or `git reset` that the user did not explicitly ask for +- (For all of these, you should weigh alternative paths that do not require approval.) + +Note that when sandboxing is set to read-only, you'll need to request approval for any command that isn't a read. + +You will be told what filesystem sandboxing, network sandboxing, and approval mode are active in a developer or user message. If you are not told about this, assume that you are running with workspace-write, network sandboxing ON, and approval on-failure. + +## Ambition vs. precision + +For tasks that have no prior context (i.e. the user is starting something brand new), you should feel free to be ambitious and demonstrate creativity with your implementation. + +If you're operating in an existing codebase, you should make sure you do exactly what the user asks with surgical precision. Treat the surrounding codebase with respect, and don't overstep (i.e. changing filenames or variables unnecessarily). You should balance being sufficiently ambitious and proactive when completing tasks of this nature. + +You should use judicious initiative to decide on the right level of detail and complexity to deliver based on the user's needs. This means showing good judgment that you're capable of doing the right extras without gold-plating. This might be demonstrated by high-value, creative touches when scope of the task is vague; while being surgical and targeted when scope is tightly specified. + +## Sharing progress updates + +For especially longer tasks that you work on (i.e. requiring many tool calls, or a plan with multiple steps), you should provide progress updates back to the user at reasonable intervals. These updates should be structured as a concise sentence or two (no more than 8-10 words long) recapping progress so far in plain language: this update demonstrates your understanding of what needs to be done, progress so far (i.e. files explores, subtasks complete), and where you're going next. + +Before doing large chunks of work that may incur latency as experienced by the user (i.e. writing a new file), you should send a concise message to the user with an update indicating what you're about to do to ensure they know what you're spending time on. Don't start editing or writing large files before informing the user what you are doing and why. + +The messages you send before tool calls should describe what is immediately about to be done next in very concise language. If there was previous work done, this preamble message should also include a note about the work done so far to bring the user along. + +## Presenting your work and final message + +Your final message should read naturally, like an update from a concise teammate. For casual conversation, brainstorming tasks, or quick questions from the user, respond in a friendly, conversational tone. You should ask questions, suggest ideas, and adapt to the user’s style. If you've finished a large amount of work, when describing what you've done to the user, you should follow the final answer formatting guidelines to communicate substantive changes. You don't need to add structured formatting for one-word answers, greetings, or purely conversational exchanges. + +You can skip heavy formatting for single, simple actions or confirmations. In these cases, respond in plain sentences with any relevant next step or quick option. Reserve multi-section structured responses for results that need grouping or explanation. + +The user is working on the same computer as you, and has access to your work. As such there's no need to show the full contents of large files you have already written unless the user explicitly asks for them. Similarly, if you've created or modified files using `apply_patch`, there's no need to tell users to "save the file" or "copy the code into a file"—just reference the file path. + +If there's something that you think you could help with as a logical next step, concisely ask the user if they want you to do so. Good examples of this are running tests, committing changes, or building out the next logical component. If there’s something that you couldn't do (even with approval) but that the user might want to do (such as verifying changes by running the app), include those instructions succinctly. + +Brevity is very important as a default. You should be very concise (i.e. no more than 10 lines), but can relax this requirement for tasks where additional detail and comprehensiveness is important for the user's understanding. + +### Final answer structure and style guidelines + +You are producing plain text that will later be styled by the CLI. Follow these rules exactly. Formatting should make results easy to scan, but not feel mechanical. Use judgment to decide how much structure adds value. + +**Section Headers** +- Use only when they improve clarity — they are not mandatory for every answer. +- Choose descriptive names that fit the content +- Keep headers short (1–3 words) and in `**Title Case**`. Always start headers with `**` and end with `**` +- Leave no blank line before the first bullet under a header. +- Section headers should only be used where they genuinely improve scanability; avoid fragmenting the answer. + +**Bullets** +- Use `-` followed by a space for every bullet. +- Bold the keyword, then colon + concise description. +- Merge related points when possible; avoid a bullet for every trivial detail. +- Keep bullets to one line unless breaking for clarity is unavoidable. +- Group into short lists (4–6 bullets) ordered by importance. +- Use consistent keyword phrasing and formatting across sections. + +**Monospace** +- Wrap all commands, file paths, env vars, and code identifiers in backticks (`` `...` ``). +- Apply to inline examples and to bullet keywords if the keyword itself is a literal file/command. +- Never mix monospace and bold markers; choose one based on whether it’s a keyword (`**`) or inline code/path (`` ` ``). + +**Structure** +- Place related bullets together; don’t mix unrelated concepts in the same section. +- Order sections from general → specific → supporting info. +- For subsections (e.g., “Binaries” under “Rust Workspace”), introduce with a bolded keyword bullet, then list items under it. +- Match structure to complexity: + - Multi-part or detailed results → use clear headers and grouped bullets. + - Simple results → minimal headers, possibly just a short list or paragraph. + +**Tone** +- Keep the voice collaborative and natural, like a coding partner handing off work. +- Be concise and factual — no filler or conversational commentary and avoid unnecessary repetition +- Use present tense and active voice (e.g., “Runs tests” not “This will run tests”). +- Keep descriptions self-contained; don’t refer to “above” or “below”. +- Use parallel structure in lists for consistency. + +**Don’t** +- Don’t use literal words “bold” or “monospace” in the content. +- Don’t nest bullets or create deep hierarchies. +- Don’t output ANSI escape codes directly — the CLI renderer applies them. +- Don’t cram unrelated keywords into a single bullet; split for clarity. +- Don’t let keyword lists run long — wrap or reformat for scanability. + +Generally, ensure your final answers adapt their shape and depth to the request. For example, answers to code explanations should have a precise, structured explanation with code references that answer the question directly. For tasks with a simple implementation, lead with the outcome and supplement only with what’s needed for clarity. Larger changes can be presented as a logical walkthrough of your approach, grouping related steps, explaining rationale where it adds value, and highlighting next actions to accelerate the user. Your answers should provide the right level of detail while being easily scannable. + +For casual greetings, acknowledgements, or other one-off conversational messages that are not delivering substantive information or structured results, respond naturally without section headers or bullet formatting. + +# Tools + +## `apply_patch` + +Your patch language is a stripped‑down, file‑oriented diff format designed to be easy to parse and safe to apply. You can think of it as a high‑level envelope: + +**_ Begin Patch [ one or more file sections ] -*** End Patch +_** End Patch Within that envelope, you get a sequence of file operations. You MUST include a header to specify the action you are taking. Each operation starts with one of three headers: -*** Add File: - create a new file. Every following line is a + line (the initial contents). -*** Delete File: - remove an existing file. Nothing follows. +**_ Add File: - create a new file. Every following line is a + line (the initial contents). +_** Delete File: - remove an existing file. Nothing follows. \*\*\* Update File: - patch an existing file in place (optionally with a rename). May be immediately followed by \*\*\* Move to: if you want to rename the file. @@ -77,60 +281,46 @@ Within a hunk each line starts with: At the end of a truncated hunk you can emit \*\*\* End of File. Patch := Begin { FileOp } End -Begin := "*** Begin Patch" NEWLINE -End := "*** End Patch" NEWLINE +Begin := "**_ Begin Patch" NEWLINE +End := "_** End Patch" NEWLINE FileOp := AddFile | DeleteFile | UpdateFile -AddFile := "*** Add File: " path NEWLINE { "+" line NEWLINE } -DeleteFile := "*** Delete File: " path NEWLINE -UpdateFile := "*** Update File: " path NEWLINE [ MoveTo ] { Hunk } -MoveTo := "*** Move to: " newPath NEWLINE +AddFile := "**_ Add File: " path NEWLINE { "+" line NEWLINE } +DeleteFile := "_** Delete File: " path NEWLINE +UpdateFile := "**_ Update File: " path NEWLINE [ MoveTo ] { Hunk } +MoveTo := "_** Move to: " newPath NEWLINE Hunk := "@@" [ header ] NEWLINE { HunkLine } [ "*** End of File" NEWLINE ] HunkLine := (" " | "-" | "+") text NEWLINE A full patch can combine several operations: -*** Begin Patch -*** Add File: hello.txt +**_ Begin Patch +_** Add File: hello.txt +Hello world -*** Update File: src/app.py -*** Move to: src/main.py +**_ Update File: src/app.py +_** Move to: src/main.py @@ def greet(): -print("Hi") +print("Hello, world!") -*** Delete File: obsolete.txt -*** End Patch +**_ Delete File: obsolete.txt +_** End Patch It is important to remember: - You must include a header with your intended action (Add/Delete/Update) - You must prefix new lines with `+` even when creating a new file -- You must follow this schema exactly when providing a patch -You can invoke apply_patch with the following shell command: +You can invoke apply_patch like: ``` shell {"command":["apply_patch","*** Begin Patch\n*** Add File: hello.txt\n+Hello, world!\n*** End Patch\n"]} ``` -## Sandbox environment and approval instructions +## `update_plan` -You are running in a sandboxed workspace backed by version control. The sandbox might be configured by the user to restrict certain behaviors, like accessing the internet or writing to files outside the current directory. +A tool named `update_plan` is available to you. You can use it to keep an up‑to‑date, step‑by‑step plan for the task. -Commands that are blocked by sandbox settings will be automatically sent to the user for approval. The result of the request will be returned (i.e. the command result, or the request denial). -The user also has an opportunity to approve the same command for the rest of the session. +To create a new plan, call `update_plan` with a short list of 1‑sentence steps (no more than 5-7 words each) with a `status` for each step (`pending`, `in_progress`, or `completed`). -Guidance on running within the sandbox: -- When running commands that will likely require approval, attempt to use simple, precise commands, to reduce frequency of approval requests. -- When approval is denied or a command fails due to a permission error, do not retry the exact command in a different way. Move on and continue trying to address the user's request. - - -## Tools available -### Plan updates - -A tool named `update_plan` is available. Use it to keep an up‑to‑date, step‑by‑step plan for the task so you can follow your progress. When making your plans, keep in mind that you are a deployed coding agent - `update_plan` calls should not involve doing anything that you aren't capable of doing. For example, `update_plan` calls should NEVER contain tasks to merge your own pull requests. Only stop to ask the user if you genuinely need their feedback on a change. - -- At the start of any nontrivial task, call `update_plan` with an initial plan: a short list of 1‑sentence steps with a `status` for each step (`pending`, `in_progress`, or `completed`). There should always be exactly one `in_progress` step until everything is done. -- Whenever you finish a step, call `update_plan` again, marking the finished step as `completed` and the next step as `in_progress`. -- If your plan needs to change, call `update_plan` with the revised steps and include an `explanation` describing the change. -- When all steps are complete, make a final `update_plan` call with all steps marked `completed`. +When steps have been completed, use `update_plan` to mark each finished step as `completed` and the next step you are working on as `in_progress`. There should always be exactly one `in_progress` step until everything is done. You can mark multiple items as complete in a single `update_plan` call. +If all steps are complete, ensure you call `update_plan` to mark all steps as `completed`. From c87fb83d8136e1d1e74be981a0324dd51906ddc9 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Thu, 7 Aug 2025 05:17:18 -0700 Subject: [PATCH 0081/1309] Calculate remaining context based on last token usage (#1940) We should only take last request size (in tokens) into account --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 15 ++++++---- codex-rs/tui/src/bottom_pane/mod.rs | 5 ++-- codex-rs/tui/src/chatwidget.rs | 29 ++++++++++++------- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 3be3f14d6f..01ee6a883a 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -45,7 +45,8 @@ pub enum InputResult { } struct TokenUsageInfo { - token_usage: TokenUsage, + total_token_usage: TokenUsage, + last_token_usage: TokenUsage, model_context_window: Option, } @@ -129,11 +130,13 @@ impl ChatComposer { /// context when the composer is empty. pub(crate) fn set_token_usage( &mut self, - token_usage: TokenUsage, + total_token_usage: TokenUsage, + last_token_usage: TokenUsage, model_context_window: Option, ) { self.token_usage_info = Some(TokenUsageInfo { - token_usage, + total_token_usage, + last_token_usage, model_context_window, }); } @@ -694,16 +697,18 @@ impl WidgetRef for &ChatComposer { // Append token/context usage info to the footer hints when available. if let Some(token_usage_info) = &self.token_usage_info { - let token_usage = &token_usage_info.token_usage; + let token_usage = &token_usage_info.total_token_usage; hint.push(Span::from(" ")); hint.push( Span::from(format!("{} tokens used", token_usage.total_tokens)) .style(Style::default().add_modifier(Modifier::DIM)), ); + let last_token_usage = &token_usage_info.last_token_usage; if let Some(context_window) = token_usage_info.model_context_window { let percent_remaining: u8 = if context_window > 0 { let percent = 100.0 - - (token_usage.total_tokens as f32 / context_window as f32 * 100.0); + - (last_token_usage.total_tokens as f32 / context_window as f32 + * 100.0); percent.clamp(0.0, 100.0) as u8 } else { 100 diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index ff3cf2f2c4..0c8610470c 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -290,11 +290,12 @@ impl BottomPane<'_> { /// is forwarded directly to the underlying `ChatComposer`. pub(crate) fn set_token_usage( &mut self, - token_usage: TokenUsage, + total_token_usage: TokenUsage, + last_token_usage: TokenUsage, model_context_window: Option, ) { self.composer - .set_token_usage(token_usage, model_context_window); + .set_token_usage(total_token_usage, last_token_usage, model_context_window); self.request_redraw(); } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 2cadc13c49..8a47353cbf 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -66,7 +66,8 @@ pub(crate) struct ChatWidget<'a> { active_history_cell: Option, config: Config, initial_user_message: Option, - token_usage: TokenUsage, + total_token_usage: TokenUsage, + last_token_usage: TokenUsage, reasoning_buffer: String, content_buffer: String, // Buffer for streaming assistant answer text; we do not surface partial @@ -213,7 +214,8 @@ impl ChatWidget<'_> { initial_prompt.unwrap_or_default(), initial_images, ), - token_usage: TokenUsage::default(), + total_token_usage: TokenUsage::default(), + last_token_usage: TokenUsage::default(), reasoning_buffer: String::new(), content_buffer: String::new(), answer_buffer: String::new(), @@ -365,9 +367,13 @@ impl ChatWidget<'_> { self.request_redraw(); } EventMsg::TokenCount(token_usage) => { - self.token_usage = add_token_usage(&self.token_usage, &token_usage); - self.bottom_pane - .set_token_usage(self.token_usage.clone(), self.config.model_context_window); + self.total_token_usage = add_token_usage(&self.total_token_usage, &token_usage); + self.last_token_usage = token_usage; + self.bottom_pane.set_token_usage( + self.total_token_usage.clone(), + self.last_token_usage.clone(), + self.config.model_context_window, + ); } EventMsg::Error(ErrorEvent { message }) => { self.add_to_history(HistoryCell::new_error_event(message.clone())); @@ -552,7 +558,7 @@ impl ChatWidget<'_> { pub(crate) fn add_status_output(&mut self) { self.add_to_history(HistoryCell::new_status_output( &self.config, - &self.token_usage, + &self.total_token_usage, )); } @@ -611,13 +617,16 @@ impl ChatWidget<'_> { } pub(crate) fn token_usage(&self) -> &TokenUsage { - &self.token_usage + &self.total_token_usage } pub(crate) fn clear_token_usage(&mut self) { - self.token_usage = TokenUsage::default(); - self.bottom_pane - .set_token_usage(self.token_usage.clone(), self.config.model_context_window); + self.total_token_usage = TokenUsage::default(); + self.bottom_pane.set_token_usage( + self.total_token_usage.clone(), + self.last_token_usage.clone(), + self.config.model_context_window, + ); } pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> { From 7e9ecfbc6a9b0c96147afed4026b5c91df9472a9 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Thu, 7 Aug 2025 09:07:51 -0700 Subject: [PATCH 0082/1309] Rename the model (#1942) --- codex-rs/core/src/client.rs | 9 --------- codex-rs/core/src/model_family.rs | 4 ++-- codex-rs/core/src/openai_model_info.rs | 2 +- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 0fa143fdb7..ed05fb5db0 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -127,15 +127,6 @@ impl ModelClient { let auth_mode = auth.as_ref().map(|a| a.mode); - if self.config.model_family.family == "2025-08-06-model" - && auth_mode != Some(AuthMode::ChatGPT) - { - return Err(CodexErr::UnexpectedStatus( - StatusCode::BAD_REQUEST, - "2025-08-06-model is only supported with ChatGPT auth, run `codex login status` to check your auth status and `codex login` to login with ChatGPT".to_string(), - )); - } - let store = prompt.store && auth_mode != Some(AuthMode::ChatGPT); let full_instructions = prompt.get_full_instructions(&self.config.model_family); diff --git a/codex-rs/core/src/model_family.rs b/codex-rs/core/src/model_family.rs index cadbceca1e..1245a030c6 100644 --- a/codex-rs/core/src/model_family.rs +++ b/codex-rs/core/src/model_family.rs @@ -89,9 +89,9 @@ pub fn find_family_for_model(slug: &str) -> Option { simple_model_family!(slug, "gpt-oss") } else if slug.starts_with("gpt-3.5") { simple_model_family!(slug, "gpt-3.5") - } else if slug.starts_with("2025-08-06-model") { + } else if slug.starts_with("gpt-5") { model_family!( - slug, "2025-08-06-model", + slug, "gpt-5", supports_reasoning_summaries: true, ) } else { diff --git a/codex-rs/core/src/openai_model_info.rs b/codex-rs/core/src/openai_model_info.rs index 0ce94267d3..a072d409c6 100644 --- a/codex-rs/core/src/openai_model_info.rs +++ b/codex-rs/core/src/openai_model_info.rs @@ -77,7 +77,7 @@ pub(crate) fn get_model_info(model_family: &ModelFamily) -> Option { max_output_tokens: 4_096, }), - "2025-08-06-model" => Some(ModelInfo { + "gpt-5" => Some(ModelInfo { context_window: 200_000, max_output_tokens: 100_000, }), From bc28b87c7bbb12628ffb178f917ab16c7e3607b8 Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 7 Aug 2025 09:27:38 -0700 Subject: [PATCH 0083/1309] [config] Onboarding flow with persistence (#1929) ## Summary In collaboration with @gpeal: upgrade the onboarding flow, and persist user settings. --------- Co-authored-by: Gabriel Peal --- codex-rs/Cargo.lock | 24 ++- codex-rs/core/Cargo.toml | 2 + codex-rs/core/src/config.rs | 92 ++++++++- codex-rs/core/src/protocol.rs | 2 +- codex-rs/exec/src/lib.rs | 2 +- codex-rs/tui/src/app.rs | 11 +- codex-rs/tui/src/cli.rs | 4 - codex-rs/tui/src/lib.rs | 127 ++++++++++--- .../tui/src/onboarding/continue_to_chat.rs | 10 +- codex-rs/tui/src/onboarding/git_warning.rs | 126 ------------ codex-rs/tui/src/onboarding/mod.rs | 2 +- .../tui/src/onboarding/onboarding_screen.rs | 44 +++-- .../tui/src/onboarding/trust_directory.rs | 179 ++++++++++++++++++ 13 files changed, 435 insertions(+), 190 deletions(-) delete mode 100644 codex-rs/tui/src/onboarding/git_warning.rs create mode 100644 codex-rs/tui/src/onboarding/trust_directory.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index eabd9f35db..4eddf7bd7b 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -708,6 +708,7 @@ dependencies = [ "tokio-test", "tokio-util", "toml 0.9.4", + "toml_edit 0.23.3", "tracing", "tree-sitter", "tree-sitter-bash", @@ -3273,7 +3274,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ - "toml_edit", + "toml_edit 0.22.27", ] [[package]] @@ -4800,7 +4801,7 @@ dependencies = [ "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", - "toml_edit", + "toml_edit 0.22.27", ] [[package]] @@ -4850,10 +4851,23 @@ dependencies = [ ] [[package]] -name = "toml_parser" -version = "1.0.1" +name = "toml_edit" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30" +checksum = "17d3b47e6b7a040216ae5302712c94d1cf88c95b47efa80e2c59ce96c878267e" +dependencies = [ + "indexmap 2.10.0", + "toml_datetime 0.7.0", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" dependencies = [ "winnow", ] diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index e9d6970ded..006a218abf 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -36,6 +36,7 @@ sha1 = "0.10.6" shlex = "1.3.0" similar = "2.7.0" strum_macros = "0.27.2" +tempfile = "3" thiserror = "2.0.12" time = { version = "0.3", features = ["formatting", "local-offset", "macros"] } tokio = { version = "1", features = [ @@ -47,6 +48,7 @@ tokio = { version = "1", features = [ ] } tokio-util = "0.7.14" toml = "0.9.4" +toml_edit = "0.23.3" tracing = { version = "0.1.41", features = ["log"] } tree-sitter = "0.25.8" tree-sitter-bash = "0.25.0" diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 081306dab1..723ee5f817 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -22,13 +22,17 @@ use serde::Deserialize; use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; +use tempfile::NamedTempFile; use toml::Value as TomlValue; +use toml_edit::DocumentMut; /// Maximum number of bytes of the documentation that will be embedded. Larger /// files are *silently truncated* to this size so we do not take up too much of /// the context window. pub(crate) const PROJECT_DOC_MAX_BYTES: usize = 32 * 1024; // 32 KiB +const CONFIG_TOML_FILE: &str = "config.toml"; + /// Application configuration loaded from disk and merged with overrides. #[derive(Debug, Clone, PartialEq)] pub struct Config { @@ -191,10 +195,28 @@ impl Config { } } +pub fn load_config_as_toml_with_cli_overrides( + codex_home: &Path, + cli_overrides: Vec<(String, TomlValue)>, +) -> std::io::Result { + let mut root_value = load_config_as_toml(codex_home)?; + + for (path, value) in cli_overrides.into_iter() { + apply_toml_override(&mut root_value, &path, value); + } + + let cfg: ConfigToml = root_value.try_into().map_err(|e| { + tracing::error!("Failed to deserialize overridden config: {e}"); + std::io::Error::new(std::io::ErrorKind::InvalidData, e) + })?; + + Ok(cfg) +} + /// Read `CODEX_HOME/config.toml` and return it as a generic TOML value. Returns /// an empty TOML table when the file does not exist. -fn load_config_as_toml(codex_home: &Path) -> std::io::Result { - let config_path = codex_home.join("config.toml"); +pub fn load_config_as_toml(codex_home: &Path) -> std::io::Result { + let config_path = codex_home.join(CONFIG_TOML_FILE); match std::fs::read_to_string(&config_path) { Ok(contents) => match toml::from_str::(&contents) { Ok(val) => Ok(val), @@ -214,6 +236,35 @@ fn load_config_as_toml(codex_home: &Path) -> std::io::Result { } } +/// Patch `CODEX_HOME/config.toml` project state. +/// Use with caution. +pub fn set_project_trusted(codex_home: &Path, project_path: &Path) -> anyhow::Result<()> { + let config_path = codex_home.join(CONFIG_TOML_FILE); + // Parse existing config if present; otherwise start a new document. + let mut doc = match std::fs::read_to_string(config_path.clone()) { + Ok(s) => s.parse::()?, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(), + Err(e) => return Err(e.into()), + }; + + // Mark the project as trusted. toml_edit is very good at handling + // missing properties + let project_key = project_path.to_string_lossy().to_string(); + doc["projects"][project_key.as_str()]["trust_level"] = toml_edit::value("trusted"); + + // ensure codex_home exists + std::fs::create_dir_all(codex_home)?; + + // create a tmp_file + let tmp_file = NamedTempFile::new_in(codex_home)?; + std::fs::write(tmp_file.path(), doc.to_string())?; + + // atomically move the tmp file into config.toml + tmp_file.persist(config_path)?; + + Ok(()) +} + /// Apply a single dotted-path override onto a TOML value. fn apply_toml_override(root: &mut TomlValue, path: &str, value: TomlValue) { use toml::value::Table; @@ -350,6 +401,13 @@ pub struct ConfigToml { /// The value for the `originator` header included with Responses API requests. pub internal_originator: Option, + + pub projects: Option>, +} + +#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct ProjectConfig { + pub trust_level: Option, } impl ConfigToml { @@ -377,6 +435,36 @@ impl ConfigToml { SandboxMode::DangerFullAccess => SandboxPolicy::DangerFullAccess, } } + + pub fn is_cwd_trusted(&self, resolved_cwd: &Path) -> bool { + let projects = self.projects.clone().unwrap_or_default(); + + projects + .get(&resolved_cwd.to_string_lossy().to_string()) + .map(|p| p.trust_level.clone().unwrap_or("".to_string()) == "trusted") + .unwrap_or(false) + } + + pub fn get_config_profile( + &self, + override_profile: Option, + ) -> Result { + let profile = override_profile.or_else(|| self.profile.clone()); + + match profile { + Some(key) => { + if let Some(profile) = self.profiles.get(key.as_str()) { + return Ok(profile.clone()); + } + + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("config profile `{key}` not found"), + )) + } + None => Ok(ConfigProfile::default()), + } + } } /// Optional overrides for user configuration (e.g., from CLI flags). diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs index c789798bcd..9008ad307d 100644 --- a/codex-rs/core/src/protocol.rs +++ b/codex-rs/core/src/protocol.rs @@ -139,7 +139,6 @@ pub enum AskForApproval { /// Under this policy, only "known safe" commands—as determined by /// `is_safe_command()`—that **only read files** are auto‑approved. /// Everything else will ask the user to approve. - #[default] #[serde(rename = "untrusted")] #[strum(serialize = "untrusted")] UnlessTrusted, @@ -151,6 +150,7 @@ pub enum AskForApproval { OnFailure, /// The model decides when to ask the user for approval. + #[default] OnRequest, /// Never ask the user to approve commands. Failures are immediately returned diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 5d7f1281ee..6ed57898b2 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -181,7 +181,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any event_processor.print_config_summary(&config, &prompt); if !skip_git_repo_check && !is_inside_git_repo(&config.cwd.to_path_buf()) { - eprintln!("Not inside a Git repo and --skip-git-repo-check was not specified."); + eprintln!("Not inside a trusted directory and --skip-git-repo-check was not specified."); std::process::exit(1); } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 0e38aba3d7..86d7414151 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -13,7 +13,6 @@ use codex_core::config::Config; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; use codex_core::protocol::Op; -use codex_core::util::is_inside_git_repo; use color_eyre::eyre::Result; use crossterm::SynchronizedUpdate; use crossterm::event::KeyCode; @@ -71,7 +70,7 @@ pub(crate) struct App<'a> { /// deferred until after the Git warning screen is dismissed. #[derive(Clone, Debug)] pub(crate) struct ChatWidgetArgs { - config: Config, + pub(crate) config: Config, initial_prompt: Option, initial_images: Vec, enhanced_keys_supported: bool, @@ -81,8 +80,8 @@ impl App<'_> { pub(crate) fn new( config: Config, initial_prompt: Option, - skip_git_repo_check: bool, initial_images: Vec, + show_trust_screen: bool, ) -> Self { let (app_event_tx, app_event_rx) = channel(); let app_event_tx = AppEventSender::new(app_event_tx); @@ -134,9 +133,7 @@ impl App<'_> { } let show_login_screen = should_show_login_screen(&config); - let show_git_warning = - !skip_git_repo_check && !is_inside_git_repo(&config.cwd.to_path_buf()); - let app_state = if show_login_screen || show_git_warning { + let app_state = if show_login_screen || show_trust_screen { let chat_widget_args = ChatWidgetArgs { config: config.clone(), initial_prompt, @@ -149,7 +146,7 @@ impl App<'_> { codex_home: config.codex_home.clone(), cwd: config.cwd.clone(), show_login_screen, - show_git_warning, + show_trust_screen, chat_widget_args, }), } diff --git a/codex-rs/tui/src/cli.rs b/codex-rs/tui/src/cli.rs index 078936dc33..91ee9cfdc7 100644 --- a/codex-rs/tui/src/cli.rs +++ b/codex-rs/tui/src/cli.rs @@ -54,10 +54,6 @@ pub struct Cli { #[clap(long = "cd", short = 'C', value_name = "DIR")] pub cwd: Option, - /// Allow running Codex outside a Git repository. - #[arg(long = "skip-git-repo-check", default_value_t = false)] - pub skip_git_repo_check: bool, - #[clap(skip)] pub config_overrides: CliConfigOverrides, } diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 0e809afdbe..057d25168b 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -6,8 +6,12 @@ use app::App; use codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID; use codex_core::config::Config; use codex_core::config::ConfigOverrides; +use codex_core::config::ConfigToml; +use codex_core::config::find_codex_home; +use codex_core::config::load_config_as_toml_with_cli_overrides; use codex_core::config_types::SandboxMode; use codex_core::protocol::AskForApproval; +use codex_core::protocol::SandboxPolicy; use codex_login::load_auth; use codex_ollama::DEFAULT_OSS_MODEL; use log_layer::TuiLogLayer; @@ -89,33 +93,38 @@ pub async fn run_main( None }; - let config = { + // canonicalize the cwd + let cwd = cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p)); + + let overrides = ConfigOverrides { + model, + approval_policy, + sandbox_mode, + cwd, + model_provider: model_provider_override, + config_profile: cli.config_profile.clone(), + codex_linux_sandbox_exe, + base_instructions: None, + include_plan_tool: Some(true), + disable_response_storage: cli.oss.then_some(true), + show_raw_agent_reasoning: cli.oss.then_some(true), + }; + + // Parse `-c` overrides from the CLI. + let cli_kv_overrides = match cli.config_overrides.parse_overrides() { + Ok(v) => v, + #[allow(clippy::print_stderr)] + Err(e) => { + eprintln!("Error parsing -c overrides: {e}"); + std::process::exit(1); + } + }; + + let mut config = { // Load configuration and support CLI overrides. - let overrides = ConfigOverrides { - model, - approval_policy, - sandbox_mode, - cwd: cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p)), - model_provider: model_provider_override, - config_profile: cli.config_profile.clone(), - codex_linux_sandbox_exe, - base_instructions: None, - include_plan_tool: Some(true), - disable_response_storage: cli.oss.then_some(true), - show_raw_agent_reasoning: cli.oss.then_some(true), - }; - // Parse `-c` overrides from the CLI. - let cli_kv_overrides = match cli.config_overrides.parse_overrides() { - Ok(v) => v, - #[allow(clippy::print_stderr)] - Err(e) => { - eprintln!("Error parsing -c overrides: {e}"); - std::process::exit(1); - } - }; #[allow(clippy::print_stderr)] - match Config::load_with_cli_overrides(cli_kv_overrides, overrides) { + match Config::load_with_cli_overrides(cli_kv_overrides.clone(), overrides) { Ok(config) => config, Err(err) => { eprintln!("Error loading configuration: {err}"); @@ -124,6 +133,34 @@ pub async fn run_main( } }; + // we load config.toml here to determine project state. + #[allow(clippy::print_stderr)] + let config_toml = { + let codex_home = match find_codex_home() { + Ok(codex_home) => codex_home, + Err(err) => { + eprintln!("Error finding codex home: {err}"); + std::process::exit(1); + } + }; + + match load_config_as_toml_with_cli_overrides(&codex_home, cli_kv_overrides) { + Ok(config_toml) => config_toml, + Err(err) => { + eprintln!("Error loading config.toml: {err}"); + std::process::exit(1); + } + } + }; + + let should_show_trust_screen = determine_repo_trust_state( + &mut config, + &config_toml, + approval_policy, + sandbox_mode, + cli.config_profile.clone(), + )?; + let log_dir = codex_core::config::log_dir(&config)?; std::fs::create_dir_all(&log_dir)?; // Open (or create) your log file, appending to it. @@ -204,12 +241,14 @@ pub async fn run_main( eprintln!(""); } - run_ratatui_app(cli, config, log_rx).map_err(|err| std::io::Error::other(err.to_string())) + run_ratatui_app(cli, config, should_show_trust_screen, log_rx) + .map_err(|err| std::io::Error::other(err.to_string())) } fn run_ratatui_app( cli: Cli, config: Config, + should_show_trust_screen: bool, mut log_rx: tokio::sync::mpsc::UnboundedReceiver, ) -> color_eyre::Result { color_eyre::install()?; @@ -227,7 +266,7 @@ fn run_ratatui_app( terminal.clear()?; let Cli { prompt, images, .. } = cli; - let mut app = App::new(config.clone(), prompt, cli.skip_git_repo_check, images); + let mut app = App::new(config.clone(), prompt, images, should_show_trust_screen); // Bridge log receiver into the AppEvent channel so latest log lines update the UI. { @@ -277,3 +316,39 @@ fn should_show_login_screen(config: &Config) -> bool { false } } + +/// Determine if user has configured a sandbox / approval policy, +/// or if the current cwd project is trusted, and updates the config +/// accordingly. +fn determine_repo_trust_state( + config: &mut Config, + config_toml: &ConfigToml, + approval_policy_overide: Option, + sandbox_mode_override: Option, + config_profile_override: Option, +) -> std::io::Result { + let config_profile = config_toml.get_config_profile(config_profile_override)?; + + if approval_policy_overide.is_some() || sandbox_mode_override.is_some() { + // if the user has overridden either approval policy or sandbox mode, + // skip the trust flow + Ok(false) + } else if config_profile.approval_policy.is_some() { + // if the user has specified settings in a config profile, skip the trust flow + // todo: profile sandbox mode? + Ok(false) + } else if config_toml.approval_policy.is_some() || config_toml.sandbox_mode.is_some() { + // if the user has specified either approval policy or sandbox mode in config.toml + // skip the trust flow + Ok(false) + } else if config_toml.is_cwd_trusted(&config.cwd) { + // if the current cwd project is trusted and no config has been set + // skip the trust flow and set the approval policy and sandbox mode + config.approval_policy = AskForApproval::OnRequest; + config.sandbox_policy = SandboxPolicy::new_workspace_write_policy(); + Ok(false) + } else { + // if none of the above conditions are met, show the trust screen + Ok(true) + } +} diff --git a/codex-rs/tui/src/onboarding/continue_to_chat.rs b/codex-rs/tui/src/onboarding/continue_to_chat.rs index 071d0851da..01e31d900a 100644 --- a/codex-rs/tui/src/onboarding/continue_to_chat.rs +++ b/codex-rs/tui/src/onboarding/continue_to_chat.rs @@ -8,12 +8,14 @@ use crate::app_event_sender::AppEventSender; use crate::onboarding::onboarding_screen::StepStateProvider; use super::onboarding_screen::StepState; +use std::sync::Arc; +use std::sync::Mutex; /// This doesn't render anything explicitly but serves as a signal that we made it to the end and /// we should continue to the chat. pub(crate) struct ContinueToChatWidget { pub event_tx: AppEventSender, - pub chat_widget_args: ChatWidgetArgs, + pub chat_widget_args: Arc>, } impl StepStateProvider for ContinueToChatWidget { @@ -24,7 +26,9 @@ impl StepStateProvider for ContinueToChatWidget { impl WidgetRef for &ContinueToChatWidget { fn render_ref(&self, _area: Rect, _buf: &mut Buffer) { - self.event_tx - .send(AppEvent::OnboardingComplete(self.chat_widget_args.clone())); + if let Ok(args) = self.chat_widget_args.lock() { + self.event_tx + .send(AppEvent::OnboardingComplete(args.clone())); + } } } diff --git a/codex-rs/tui/src/onboarding/git_warning.rs b/codex-rs/tui/src/onboarding/git_warning.rs deleted file mode 100644 index e4e5747404..0000000000 --- a/codex-rs/tui/src/onboarding/git_warning.rs +++ /dev/null @@ -1,126 +0,0 @@ -use std::path::PathBuf; - -use codex_core::util::is_inside_git_repo; -use crossterm::event::KeyCode; -use crossterm::event::KeyEvent; -use ratatui::buffer::Buffer; -use ratatui::layout::Rect; -use ratatui::prelude::Widget; -use ratatui::style::Modifier; -use ratatui::style::Style; -use ratatui::style::Stylize; -use ratatui::text::Line; -use ratatui::text::Span; -use ratatui::widgets::Paragraph; -use ratatui::widgets::WidgetRef; -use ratatui::widgets::Wrap; - -use crate::app_event::AppEvent; -use crate::app_event_sender::AppEventSender; -use crate::colors::LIGHT_BLUE; - -use crate::onboarding::onboarding_screen::KeyboardHandler; -use crate::onboarding::onboarding_screen::StepStateProvider; - -use super::onboarding_screen::StepState; - -pub(crate) struct GitWarningWidget { - pub event_tx: AppEventSender, - pub cwd: PathBuf, - pub selection: Option, - pub highlighted: GitWarningSelection, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum GitWarningSelection { - Continue, - Exit, -} - -impl WidgetRef for &GitWarningWidget { - fn render_ref(&self, area: Rect, buf: &mut Buffer) { - let mut lines: Vec = vec![ - Line::from(vec![ - Span::raw("> "), - Span::raw("You are running Codex in "), - Span::styled( - self.cwd.to_string_lossy().to_string(), - Style::default().add_modifier(Modifier::BOLD), - ), - Span::raw(". This folder is not version controlled."), - ]), - Line::from(""), - Line::from(" Do you want to continue?"), - Line::from(""), - ]; - - let create_option = - |idx: usize, option: GitWarningSelection, text: &str| -> Line<'static> { - let is_selected = self.highlighted == option; - if is_selected { - Line::from(vec![ - Span::styled( - format!("> {}. ", idx + 1), - Style::default().fg(LIGHT_BLUE).add_modifier(Modifier::DIM), - ), - Span::styled(text.to_owned(), Style::default().fg(LIGHT_BLUE)), - ]) - } else { - Line::from(format!(" {}. {}", idx + 1, text)) - } - }; - - lines.push(create_option(0, GitWarningSelection::Continue, "Yes")); - lines.push(create_option(1, GitWarningSelection::Exit, "No")); - lines.push(Line::from("")); - lines.push(Line::from(" Press Enter to continue").add_modifier(Modifier::DIM)); - - Paragraph::new(lines) - .wrap(Wrap { trim: false }) - .render(area, buf); - } -} - -impl KeyboardHandler for GitWarningWidget { - fn handle_key_event(&mut self, key_event: KeyEvent) { - match key_event.code { - KeyCode::Up | KeyCode::Char('k') => { - self.highlighted = GitWarningSelection::Continue; - } - KeyCode::Down | KeyCode::Char('j') => { - self.highlighted = GitWarningSelection::Exit; - } - KeyCode::Char('1') => self.handle_continue(), - KeyCode::Char('2') => self.handle_quit(), - KeyCode::Enter => match self.highlighted { - GitWarningSelection::Continue => self.handle_continue(), - GitWarningSelection::Exit => self.handle_quit(), - }, - _ => {} - } - } -} - -impl StepStateProvider for GitWarningWidget { - fn get_step_state(&self) -> StepState { - let is_git_repo = is_inside_git_repo(&self.cwd); - match is_git_repo { - true => StepState::Hidden, - false => match self.selection { - Some(_) => StepState::Complete, - None => StepState::InProgress, - }, - } - } -} - -impl GitWarningWidget { - fn handle_continue(&mut self) { - self.selection = Some(GitWarningSelection::Continue); - } - - fn handle_quit(&mut self) { - self.highlighted = GitWarningSelection::Exit; - self.event_tx.send(AppEvent::ExitRequest); - } -} diff --git a/codex-rs/tui/src/onboarding/mod.rs b/codex-rs/tui/src/onboarding/mod.rs index 645cda22d9..c116936851 100644 --- a/codex-rs/tui/src/onboarding/mod.rs +++ b/codex-rs/tui/src/onboarding/mod.rs @@ -1,5 +1,5 @@ mod auth; mod continue_to_chat; -mod git_warning; pub mod onboarding_screen; +mod trust_directory; mod welcome; diff --git a/codex-rs/tui/src/onboarding/onboarding_screen.rs b/codex-rs/tui/src/onboarding/onboarding_screen.rs index 7ce7d16c47..a104f777c2 100644 --- a/codex-rs/tui/src/onboarding/onboarding_screen.rs +++ b/codex-rs/tui/src/onboarding/onboarding_screen.rs @@ -1,3 +1,4 @@ +use codex_core::util::is_inside_git_repo; use crossterm::event::KeyEvent; use ratatui::buffer::Buffer; use ratatui::layout::Rect; @@ -11,16 +12,18 @@ use crate::app_event_sender::AppEventSender; use crate::onboarding::auth::AuthModeWidget; use crate::onboarding::auth::SignInState; use crate::onboarding::continue_to_chat::ContinueToChatWidget; -use crate::onboarding::git_warning::GitWarningSelection; -use crate::onboarding::git_warning::GitWarningWidget; +use crate::onboarding::trust_directory::TrustDirectorySelection; +use crate::onboarding::trust_directory::TrustDirectoryWidget; use crate::onboarding::welcome::WelcomeWidget; use std::path::PathBuf; +use std::sync::Arc; +use std::sync::Mutex; #[allow(clippy::large_enum_variant)] enum Step { Welcome(WelcomeWidget), Auth(AuthModeWidget), - GitWarning(GitWarningWidget), + TrustDirectory(TrustDirectoryWidget), ContinueToChat(ContinueToChatWidget), } @@ -49,7 +52,7 @@ pub(crate) struct OnboardingScreenArgs { pub codex_home: PathBuf, pub cwd: PathBuf, pub show_login_screen: bool, - pub show_git_warning: bool, + pub show_trust_screen: bool, } impl OnboardingScreen { @@ -60,7 +63,7 @@ impl OnboardingScreen { codex_home, cwd, show_login_screen, - show_git_warning, + show_trust_screen, } = args; let mut steps: Vec = vec![Step::Welcome(WelcomeWidget { is_logged_in: !show_login_screen, @@ -71,20 +74,33 @@ impl OnboardingScreen { highlighted_mode: AuthMode::ChatGPT, error: None, sign_in_state: SignInState::PickMode, - codex_home, + codex_home: codex_home.clone(), })) } - if show_git_warning { - steps.push(Step::GitWarning(GitWarningWidget { - event_tx: event_tx.clone(), + let is_git_repo = is_inside_git_repo(&cwd); + let highlighted = if is_git_repo { + TrustDirectorySelection::Trust + } else { + // Default to not trusting the directory if it's not a git repo. + TrustDirectorySelection::DontTrust + }; + // Share ChatWidgetArgs between steps so changes in the TrustDirectory step + // are reflected when continuing to chat. + let shared_chat_args = Arc::new(Mutex::new(chat_widget_args)); + if show_trust_screen { + steps.push(Step::TrustDirectory(TrustDirectoryWidget { cwd, + codex_home, + is_git_repo, selection: None, - highlighted: GitWarningSelection::Continue, + highlighted, + error: None, + chat_widget_args: shared_chat_args.clone(), })) } steps.push(Step::ContinueToChat(ContinueToChatWidget { event_tx: event_tx.clone(), - chat_widget_args, + chat_widget_args: shared_chat_args, })); // TODO: add git warning. Self { event_tx, steps } @@ -215,7 +231,7 @@ impl KeyboardHandler for Step { match self { Step::Welcome(_) | Step::ContinueToChat(_) => (), Step::Auth(widget) => widget.handle_key_event(key_event), - Step::GitWarning(widget) => widget.handle_key_event(key_event), + Step::TrustDirectory(widget) => widget.handle_key_event(key_event), } } } @@ -225,7 +241,7 @@ impl StepStateProvider for Step { match self { Step::Welcome(w) => w.get_step_state(), Step::Auth(w) => w.get_step_state(), - Step::GitWarning(w) => w.get_step_state(), + Step::TrustDirectory(w) => w.get_step_state(), Step::ContinueToChat(w) => w.get_step_state(), } } @@ -240,7 +256,7 @@ impl WidgetRef for Step { Step::Auth(widget) => { widget.render_ref(area, buf); } - Step::GitWarning(widget) => { + Step::TrustDirectory(widget) => { widget.render_ref(area, buf); } Step::ContinueToChat(widget) => { diff --git a/codex-rs/tui/src/onboarding/trust_directory.rs b/codex-rs/tui/src/onboarding/trust_directory.rs new file mode 100644 index 0000000000..3be9bac1ac --- /dev/null +++ b/codex-rs/tui/src/onboarding/trust_directory.rs @@ -0,0 +1,179 @@ +use std::path::PathBuf; + +use codex_core::config::set_project_trusted; +use codex_core::protocol::AskForApproval; +use codex_core::protocol::SandboxPolicy; +use crossterm::event::KeyCode; +use crossterm::event::KeyEvent; +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::prelude::Widget; +use ratatui::style::Color; +use ratatui::style::Modifier; +use ratatui::style::Style; +use ratatui::style::Stylize; +use ratatui::text::Line; +use ratatui::text::Span; +use ratatui::widgets::Paragraph; +use ratatui::widgets::WidgetRef; +use ratatui::widgets::Wrap; + +use crate::colors::LIGHT_BLUE; + +use crate::onboarding::onboarding_screen::KeyboardHandler; +use crate::onboarding::onboarding_screen::StepStateProvider; + +use super::onboarding_screen::StepState; +use crate::app::ChatWidgetArgs; +use std::sync::Arc; +use std::sync::Mutex; + +pub(crate) struct TrustDirectoryWidget { + pub codex_home: PathBuf, + pub cwd: PathBuf, + pub is_git_repo: bool, + pub selection: Option, + pub highlighted: TrustDirectorySelection, + pub error: Option, + pub chat_widget_args: Arc>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum TrustDirectorySelection { + Trust, + DontTrust, +} + +impl WidgetRef for &TrustDirectoryWidget { + fn render_ref(&self, area: Rect, buf: &mut Buffer) { + let mut lines: Vec = vec![ + Line::from(vec![ + Span::raw("> "), + Span::styled( + "You are running Codex in ", + Style::default().add_modifier(Modifier::BOLD), + ), + Span::raw(self.cwd.to_string_lossy().to_string()), + ]), + Line::from(""), + ]; + + if self.is_git_repo { + lines.push(Line::from( + " Since this folder is version controlled, you may wish to allow Codex", + )); + lines.push(Line::from( + " to work in this folder without asking for approval.", + )); + } else { + lines.push(Line::from( + " Since this folder is not version controlled, we recommend requiring", + )); + lines.push(Line::from(" approval of all edits and commands.")); + } + lines.push(Line::from("")); + + let create_option = + |idx: usize, option: TrustDirectorySelection, text: &str| -> Line<'static> { + let is_selected = self.highlighted == option; + if is_selected { + Line::from(vec![ + Span::styled( + format!("> {}. ", idx + 1), + Style::default().fg(LIGHT_BLUE).add_modifier(Modifier::DIM), + ), + Span::styled(text.to_owned(), Style::default().fg(LIGHT_BLUE)), + ]) + } else { + Line::from(format!(" {}. {}", idx + 1, text)) + } + }; + + if self.is_git_repo { + lines.push(create_option( + 0, + TrustDirectorySelection::Trust, + "Yes, allow Codex to work in this folder without asking for approval", + )); + lines.push(create_option( + 1, + TrustDirectorySelection::DontTrust, + "No, ask me to approve edits and commands", + )); + } else { + lines.push(create_option( + 0, + TrustDirectorySelection::Trust, + "Allow Codex to work in this folder without asking for approval", + )); + lines.push(create_option( + 1, + TrustDirectorySelection::DontTrust, + "Require approval of edits and commands", + )); + } + lines.push(Line::from("")); + if let Some(error) = &self.error { + lines.push(Line::from(format!(" {error}")).fg(Color::Red)); + lines.push(Line::from("")); + } + lines.push(Line::from(" Press Enter to continue").add_modifier(Modifier::DIM)); + + Paragraph::new(lines) + .wrap(Wrap { trim: false }) + .render(area, buf); + } +} + +impl KeyboardHandler for TrustDirectoryWidget { + fn handle_key_event(&mut self, key_event: KeyEvent) { + match key_event.code { + KeyCode::Up | KeyCode::Char('k') => { + self.highlighted = TrustDirectorySelection::Trust; + } + KeyCode::Down | KeyCode::Char('j') => { + self.highlighted = TrustDirectorySelection::DontTrust; + } + KeyCode::Char('1') => self.handle_trust(), + KeyCode::Char('2') => self.handle_dont_trust(), + KeyCode::Enter => match self.highlighted { + TrustDirectorySelection::Trust => self.handle_trust(), + TrustDirectorySelection::DontTrust => self.handle_dont_trust(), + }, + _ => {} + } + } +} + +impl StepStateProvider for TrustDirectoryWidget { + fn get_step_state(&self) -> StepState { + match self.selection { + Some(_) => StepState::Complete, + None => StepState::InProgress, + } + } +} + +impl TrustDirectoryWidget { + fn handle_trust(&mut self) { + if let Err(e) = set_project_trusted(&self.codex_home, &self.cwd) { + tracing::error!("Failed to set project trusted: {e:?}"); + self.error = Some(e.to_string()); + // self.error = Some("Failed to set project trusted".to_string()); + } + + // Update the in-memory chat config for this session to a more permissive + // policy suitable for a trusted workspace. + if let Ok(mut args) = self.chat_widget_args.lock() { + args.config.approval_policy = AskForApproval::OnRequest; + args.config.sandbox_policy = SandboxPolicy::new_workspace_write_policy(); + } + + self.selection = Some(TrustDirectorySelection::Trust); + } + + fn handle_dont_trust(&mut self) { + self.highlighted = TrustDirectorySelection::DontTrust; + self.selection = Some(TrustDirectorySelection::DontTrust); + } +} From 62ed5907f9fcdabcd87c94aeca3d293e177d3aed Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Thu, 7 Aug 2025 09:46:13 -0700 Subject: [PATCH 0084/1309] Better usage errors (#1941) image --- codex-rs/core/src/client.rs | 39 ++++++++++++++++++++++++++++++------- codex-rs/core/src/codex.rs | 1 + codex-rs/core/src/error.rs | 6 ++++++ 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index ed05fb5db0..3a709aad51 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -40,6 +40,16 @@ use crate::protocol::TokenUsage; use crate::util::backoff; use std::sync::Arc; +#[derive(Debug, Deserialize)] +struct ErrorResponse { + error: Error, +} + +#[derive(Debug, Deserialize)] +struct Error { + code: String, +} + #[derive(Clone)] pub struct ModelClient { config: Arc, @@ -225,6 +235,14 @@ impl ModelClient { } Ok(res) => { let status = res.status(); + + // Pull out Retry‑After header if present. + let retry_after_secs = res + .headers() + .get(reqwest::header::RETRY_AFTER) + .and_then(|v| v.to_str().ok()) + .and_then(|s| s.parse::().ok()); + // The OpenAI Responses endpoint returns structured JSON bodies even for 4xx/5xx // errors. When we bubble early with only the HTTP status the caller sees an opaque // "unexpected status 400 Bad Request" which makes debugging nearly impossible. @@ -238,17 +256,24 @@ impl ModelClient { return Err(CodexErr::UnexpectedStatus(status, body)); } + if status == StatusCode::TOO_MANY_REQUESTS { + let body = res.json::().await.ok(); + if let Some(ErrorResponse { + error: Error { code, .. }, + }) = body + { + if code == "usage_limit_reached" { + return Err(CodexErr::UsageLimitReached); + } else if code == "usage_not_included" { + return Err(CodexErr::UsageNotIncluded); + } + } + } + if attempt > max_retries { return Err(CodexErr::RetryLimit(status)); } - // Pull out Retry‑After header if present. - let retry_after_secs = res - .headers() - .get(reqwest::header::RETRY_AFTER) - .and_then(|v| v.to_str().ok()) - .and_then(|s| s.parse::().ok()); - let delay = retry_after_secs .map(|s| Duration::from_millis(s * 1_000)) .unwrap_or_else(|| backoff(attempt)); diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index eb1bc4f9d8..aaef73ded9 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1290,6 +1290,7 @@ async fn run_turn( Ok(output) => return Ok(output), Err(CodexErr::Interrupted) => return Err(CodexErr::Interrupted), Err(CodexErr::EnvVar(var)) => return Err(CodexErr::EnvVar(var)), + Err(e @ (CodexErr::UsageLimitReached | CodexErr::UsageNotIncluded)) => return Err(e), Err(e) => { // Use the configured provider-specific stream retry budget. let max_retries = sess.client.get_provider().stream_max_retries(); diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index 537f4a0361..1f28334666 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -62,6 +62,12 @@ pub enum CodexErr { #[error("unexpected status {0}: {1}")] UnexpectedStatus(StatusCode, String), + #[error("Usage limit has been reached")] + UsageLimitReached, + + #[error("Usage not included with the plan")] + UsageNotIncluded, + /// Retry limit exceeded. #[error("exceeded retry limit, last status: {0}")] RetryLimit(StatusCode), From 09adbf9132c5427e1325f7c9913997e9c412cd95 Mon Sep 17 00:00:00 2001 From: Ed Bayes Date: Thu, 7 Aug 2025 10:04:49 -0700 Subject: [PATCH 0085/1309] remove composer bg (#1944) passes local tests --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 01ee6a883a..2743ada547 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -35,8 +35,6 @@ const BASE_PLACEHOLDER_TEXT: &str = "Ask Codex to do anything"; /// If the pasted content exceeds this number of characters, replace it with a /// placeholder in the UI. const LARGE_PASTE_CHAR_THRESHOLD: usize = 1000; -/// Background color used for the chat composer area. -const COMPOSER_BG_COLOR: Color = Color::Black; /// Result returned when the user interacts with the text area. pub enum InputResult { @@ -743,9 +741,6 @@ impl WidgetRef for &ChatComposer { textarea_rect.width = textarea_rect.width.saturating_sub(1); textarea_rect.x += 1; - // Fill only the textarea content region with a subtle background so it - // doesn't affect the hint line or popups and remains behind the text. - buf.set_style(textarea_rect, Style::default().bg(COMPOSER_BG_COLOR)); let mut state = self.textarea_state.borrow_mut(); StatefulWidgetRef::render_ref(&(&self.textarea), textarea_rect, buf, &mut state); if self.textarea.text().is_empty() { From 107d2ce4e74618968b2eb7016777121d9529a204 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 7 Aug 2025 10:13:13 -0700 Subject: [PATCH 0086/1309] fix: change OPENAI_DEFAULT_MODEL to "gpt-5" (#1943) --- codex-rs/core/src/config.rs | 3 ++- codex-rs/core/src/flags.rs | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 723ee5f817..f9c15b9eed 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -9,7 +9,6 @@ use crate::config_types::ShellEnvironmentPolicy; use crate::config_types::ShellEnvironmentPolicyToml; use crate::config_types::Tui; use crate::config_types::UriBasedFileOpener; -use crate::flags::OPENAI_DEFAULT_MODEL; use crate::model_family::ModelFamily; use crate::model_family::find_family_for_model; use crate::model_provider_info::ModelProviderInfo; @@ -26,6 +25,8 @@ use tempfile::NamedTempFile; use toml::Value as TomlValue; use toml_edit::DocumentMut; +const OPENAI_DEFAULT_MODEL: &str = "gpt-5"; + /// Maximum number of bytes of the documentation that will be embedded. Larger /// files are *silently truncated* to this size so we do not take up too much of /// the context window. diff --git a/codex-rs/core/src/flags.rs b/codex-rs/core/src/flags.rs index c150405491..60201a3b18 100644 --- a/codex-rs/core/src/flags.rs +++ b/codex-rs/core/src/flags.rs @@ -3,7 +3,6 @@ use std::time::Duration; use env_flags::env_flags; env_flags! { - pub OPENAI_DEFAULT_MODEL: &str = "codex-mini-latest"; pub OPENAI_API_BASE: &str = "https://api.openai.com/v1"; /// Fallback when the provider-specific key is not set. From a593b1c3ab1e9ab7844e07d7a2eaf4c31eb10ed4 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Thu, 7 Aug 2025 10:20:33 -0700 Subject: [PATCH 0087/1309] Use different field for error type (#1945) --- codex-rs/core/src/client.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 3a709aad51..723247d28b 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -47,7 +47,7 @@ struct ErrorResponse { #[derive(Debug, Deserialize)] struct Error { - code: String, + r#type: String, } #[derive(Clone)] @@ -259,12 +259,12 @@ impl ModelClient { if status == StatusCode::TOO_MANY_REQUESTS { let body = res.json::().await.ok(); if let Some(ErrorResponse { - error: Error { code, .. }, + error: Error { r#type, .. }, }) = body { - if code == "usage_limit_reached" { + if r#type == "usage_limit_reached" { return Err(CodexErr::UsageLimitReached); - } else if code == "usage_not_included" { + } else if r#type == "usage_not_included" { return Err(CodexErr::UsageNotIncluded); } } From f23c3066c898d7eb1e1637b7eefe640808ba4234 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Thu, 7 Aug 2025 10:46:43 -0700 Subject: [PATCH 0088/1309] Add capacity error (#1947) --- codex-rs/core/src/client.rs | 4 ++++ codex-rs/core/src/error.rs | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 723247d28b..34aecad17a 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -271,6 +271,10 @@ impl ModelClient { } if attempt > max_retries { + if status == StatusCode::INTERNAL_SERVER_ERROR { + return Err(CodexErr::InternalServerError); + } + return Err(CodexErr::RetryLimit(status)); } diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index 1f28334666..f6394b71ce 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -68,6 +68,11 @@ pub enum CodexErr { #[error("Usage not included with the plan")] UsageNotIncluded, + #[error( + "We’re currently experiencing high demand, which may cause temporary errors. We’re adding capacity in East and West Europe to restore normal service." + )] + InternalServerError, + /// Retry limit exceeded. #[error("exceeded retry limit, last status: {0}")] RetryLimit(StatusCode), From e07776ccc974a7d8458fcf85e704bb428fd5471c Mon Sep 17 00:00:00 2001 From: Ed Bayes Date: Thu, 7 Aug 2025 11:20:53 -0700 Subject: [PATCH 0089/1309] update readme (#1948) Co-authored-by: Alexander Embiricos --- .github/codex-cli-login.png | Bin 0 -> 420219 bytes .github/codex-cli-permissions.png | Bin 0 -> 417750 bytes .github/codex-cli-splash.png | Bin 0 -> 422175 bytes README.md | 299 ++++++++++++++++-------------- 4 files changed, 155 insertions(+), 144 deletions(-) create mode 100644 .github/codex-cli-login.png create mode 100644 .github/codex-cli-permissions.png create mode 100644 .github/codex-cli-splash.png diff --git a/.github/codex-cli-login.png b/.github/codex-cli-login.png new file mode 100644 index 0000000000000000000000000000000000000000..4e59826eaf7fc60038dcfbf19369ba93b3ff5ac2 GIT binary patch literal 420219 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

5iDT2Mc7Om*`~Ifg^OnZi4c}nI12>1<*;Bq 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≫ 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 oM``MWV#t- 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

)c&yi1Hd74Z_Pf4CFthD9~TqB5%% z;CQYSt(zE6B16c`A&7wBRsOL%_rUWqTUL2OTz>UsMb1**zTS?7Z*OjR+pqH=JVk&M zqnE0D+&7zIbsX)BJK#wLG#tkn=Qm%`svjLDOblS@dTC|DI*Kr0BpF-L1XJ^gl z&XEVred0hmrsR34oGtL_qW@j~!^+R0;WX>Gr_VDY{Nw5AX{+I$GX&sPktF|Nbbcp` zS@QjVXE8a)ylN5n;3i4^xmcZh&apAggMTNmEK5h4NR%Zk24Oc5hA^>vU*Yml|sX>sEu$ zt}+)gB{fHTAjE@+5q;J5&VNo8G)IhGj^nExEWfLM5}z0~V-nLvZpg@c$aGZfp2|EX zX>d5yT>qMKsZeeSgB`P`xw5}4m%d(_5t+~hNsY`s2?h%OIxd2*-;uR@ARkZ@VeE$hc@HSw*;+`?iM8y2pVWk7g;ieDgvoX z#8lFbe4ssK;6R`>B5;VZQUL--fbQ6skihvtr_rbV7xt5W6Bup>FW9+8peql=KKkZp z#s9gDWAx)2f}<;f)x+}-Mw;M0e#Rmdcp95>>b$^*WtV?(E}Kjgr6w{9Sy#b_dmsgc z>{mI&zcb6N>v59Xb$WUDH@hf}Rgw5%+3K|f3RL&y0dJ~TgOs@; zXuABp=cTuUCpL35j#4q#Q2N_0_vr@dH6pUucbBf^(Dh^cl9e5po>CVYK|A)GJUA2< zKF2yzJDcx(T6Xhs%Y*Jx8;k?7vcps~-PsBhW7J`Ef8 zwMJ|+zFZgX5$}xiS(lkS^yz5@CsoG!oRu1%ZMNn=HTpAM8@2Gi`PO>&O;pwV{C+)R zPK|p!TRqU#JWlRyUbmh_l=g6n1A*AxAT^8*`f9A-h!fy97SoR%#0KWK6A|Q5#O!j<^Q|xz zWtI~^j&VQa1~HcdQTJXAm*Hwri{AgS$fL*hUl8N!sE*9n#i(%4^C?j|BNaOuXh4_= z-G11%f7fN+lHOB!`{+FI<*}#V>-Gu8_pGVo{8)tqkDr8lKI`;pmNy6!OQHYCSxtY( zh5GsOYIeh~E=$(dZjblwD+bcVUG1J_;lGQ^CF$8>h&te(uJHZ7-69t-TKYZpagzXI~g*;ASVTTyCvpyRm)<(Xdfcv5a3b zb7P?;AXPTHY>Z2#aBadZYU@+veCBp;!FY!uA!Mhxl{Z~n=yPl#_}+fmbB%>ra*HxR z8K9oLeIOOMEZe~aHRqn$vi8vTI0jqJj#xW90iiSc%0xUw!d#Z1v%-1iY21EocSBcoPd*EJ z3wj*}&djZ77xNt~ z$K9Z#-De`URsPD}7vpp6p6S>@PcEsR+6 zthg3l=;>;6QYTlA81-tkw-M}7Afq>pT^{hO8rWP!K&aq&n?v%6VmT8l!jO}Gx zw1Q0PZ50}GczpPHR7|oq$#@J8y3q-#2YH8^i@CoyxV&alKIeHO{n0E%BoGkQ5)aJa z|LgtiuOoWa0Qj%HgBRt)p4K=J)n)vtF^Z&SgzhIDJnhIQ`u&TPaOv{jrLyF&s963k ze{!+1AO8&FRcrc`5B_X_g2#-acZRE*5zSgCTn9aurY?*_k+G2D zC_T`@YX5xkToq;fPAE-?^JS-R=Y46}tB-B22fEJG#v(J7Jm+N*ifw7{w0fcY~rmEp{B z4x?9G2-tAzY9Z07)YvNvZI14{5w^jL!SHj~RrZ z@HANMv(IBLt~=c3F58IoGLTZ+j1eoO6RX+b=DJuKlNrazV`~kb^1Hl!zsh_2KQ|-E zKcOf1l69(pFxqrCk`@D;(=pa`vql*?=ytYW(=}XU)%szju&n`x?IR9*UMnmQpD6tWTa3lroptLj^ zi;66OmX&9^OcHL}CMRC$;N!i|Gol7Fi0`w{d(Ry#aY=sAgVGf;)dR!G zl{~~1h(7mRX7w9QgdT%HJwi%0))_IhEL#(42XSrf?HI`JbfWU81YyVRtYzn34b$*) z@#@dT@6+GoSv!3DXfULyINUg;4?3cc`}nMU#P3v|^Sm!AD@^zP+w zaDD6#Tf(24x!1e(=d4iY8Sm5H=b3ZA-=+*?%Q+2;Hy((yyIxPehPSLB|wbfdZ(M3h!2e6Ag zKuk}suUHS~vJJ&ZvIM=@AZekbyd5rtDJP6s`Y$hBEw^Nkezw_6-$h;;^m2md$tp2m2brZ!J zTpg`6`ZDyL9`S#OY)e#wat{o*G3pwQYP{m|dgv}A&rv73%)#Cl#6a?_=0gk14O3({ zm6bPEKuYWLffiS_tD$a2lS9B@SMbXPGo!zuF5l2spqc4Mu|HjZEEqNX;y=Cq#C%bF%qgHo<={ko+|Em-q^0>v%GNayR@RvaK`^R(tD6JO= zEd4GC+46pFOCLOQfpaT#_;B3I0hR-P0rEzb7f4F$;H zv9N-sW=~sLt*lGVWQo!R37t)(3eziaqSjH+F^nmgv|+@+*$R@lvsA zhXW?qC;38kM81!Y%)eEW_vitQU;Cl)oYr9b828F7@UxvNqPmPE&aT-r%NBF|r*oyJ zpCwPcD|Yg^m49KZihVH8StX|$Nfy$zSXQsAS!1m;=Bu>lU!}0H_Rc@A&zzheg(oSw zZmiuUCxOx6SI93LT!MI+)a{Uv-NDnhEBAxq5O&NL7TxHNJeb18ZQ7V8gG3M zrAfLTNNf_UnXetQYSj6^o3#g)4!4Nk`3#X}k5ji-EATAUous{zJu5rKvBY8PTT3Z~ z%;P0~YB{j_vwFE`?E@>u>70z^ec|x zDi|#C{~uibOrzD|6FFN=tosvK=+j0f-7Q_H-xdkFT$G`RVeKyWz8Tf*yXJMyEV5<12)lxIPpJDee#@mq~LE z<&tUbOHsy>5y%i4ab4QTK8Mz~h0pDG{Y6iukX4q>_0u`n_DM_^ zE0me;cjH{Y%C*f*g}Y7#bqKvJzIbt)Hd`O9^b1G3aJ_(Q4Ywa=jvC|MNhx9TV2;U~?0X@bX^6vRR3yev z&omcni~hM=({;)qqn5@S@r?6ikP7ETYtUnyqk|r|Wmdf}!${;Rz&i|Yp4lv##|T{B z!qudNvTBK{S9u>(0==- z0I3XwlZEP9_T%mA%Nt!;EG~@8{-G;&Pvr}!Gsu}HlYrau-pB&pV_lqgy&j`}^%SSDkU>6(9fx3P&f3t~1Do0>MzC zuP#$EUT_3EAARzye4vHlPI{%+%!=$8w0Se~^zXNRZSa;+tVJ7cm!1t0x7TCMaC3h3 zVDqjGcF25)5s0KxoL0bE`({uNaGg)&A=TIpgOn42>i7 z%@#&}dq*3wU)-39{xT%Kq(*AkzhS8k8<)?~!=rB1CFZ$44N>l{J;J;US4hp><p$clRb$@a^Tv*rH9= zP(Pu+N}k>t(@9tJ$xkew3Z5HCQsY5_1E6b!O={i%mI6F0m6a@q8QI8G7G6@NiFNk) zlEj=veX3D+HGswWPzWSTJ$!>!Tbaq=dqNtkNe#r~DI);{T-LL;-b>X$1uOT4OB$Rr zT?A4A|H1)^=d&Mzc0*=~+3|DE@_fE511mnW(*>$~TI8+G;onax25$XrKH;wh=E`Oh z-anHFOz4NojUgcAEN?0}6K+o)8|(c&jVa?l8zFRQ*u!ZFri2qO#}KF_Ks|Sd$OgNq zmP&`?j%t`$0&{9vj|X}jwJ?4{zyKq**D%P9UoSQFyS^c)(bOjxwaac+~189vE%Qx(oh7p zLic*w_gE{GX~kAF*)9g!_{65nf>AQy( zB)Obj4i!Cq3+^lP49m>6B5V9b3%>eSE?>vELd?bQ^(OwYfpjWJmUWz$2k-Md40t|O zIez!r*p$|NMjKlC>&TTh1^D{bS)YzP#vc4-s(A~m^*aOdU8SXf`U-_roVFWlddP1_ z2gHx&L_c^g+Ocvg&x=7CNfRedLyGS~-oE>6_dfL$-$0yoEtKHr{4|cCU|o;uuY z2ThlKY2q=TLg&K_b%|tL&{N0n7Y0;sB1Mi*nyhh%4h1>``zb^^`GqRt>RxE{5(fwj zTIe$M?sxVDJsU=(Vf5>+@gUIoiW(Wu6=PT$XOsx1(Ut$X{eX+5@M0f@MsOm_{1V%) zIo3X05vdsY1;5UCaYi@uxPX$ooxrgE`W?$a9v?o}wh(-MnRC@+ ze%G({bVsVZq{}=laZnT{Xz>@pb~#@B;$u1>4Fb|C<;F7n&G~>7u=`#6L4H->jNcZc zSsEH&8~9;lpDJentK-^b-SOSBOMiNCW;Nw@%jknSx7aHq3r=DMZ4rn5dN@Dqw@NvL zBxLeW4fC}}_2utByQcri<&lx6j0s6JDP0o7|_~Ih>w9z;x?*?FnvaIMcm3av1SzY8Jgi5A1k~hu|6y=!1T( zg7@(|pgHXDsg`%8$UlK-*yL_bjJe^6EK)w-N*6K)&@cq}z6R92a% zt)KHVxVr{pqU>QQogR7yJphjp@e((Fp2TRW=Gd8#TyN_@gNxJ0hPfPuh=w+>-$z?8 za7=^ZoW-Sg*$)*76qN+6*xY+WDPdmr*>@%(YV<^D75i ze8~fP41@69woQtnJUT9C-hlRF7u=3CmsBE_R(2?K*xc!R*6A*#P!wkc$wIdETs6=D z9{4UW(ul152K3^FM~|Zx{1rWHvNY;5ek1zk-%xfd9zenDyGx7-d8e zvsIhC`n0`e&Y~@i-^z6Dr;B(znHK$YahJb4Jx|Nhay=&&S=K}>j8*vjg21j3WE9ld z^MDE?)!Kyx$TobXdt#=uao=(d86=}9P}!0FA_O!m8_(gBx!JDXpTP_Uy6fFPgDY7m zURy(8+Oe3ufy`>)XYc38)OX!|qdR;xkbZHX0`w1(4Qh74vf|wxe9_NKm=ve z_b)_Sc?Wdz!Dd#)SK>H*6-A@{>C6pb%2fcgEMV~4_xgu2RX=_~}q)Fl9zg+py z-B4fWDX%su5{XGECxkM}GfgT}Ebefz5N+-Dz2Y766b0LZ5y@NB6%+Hy=nw#z0_CZq zX!v&&ZW6sGkQIKS#K<7tL6Q{yJ|(MF!0olaxE0Lq&VaCwS~IC4a&vc~I%{=LwX(%S zId3!Ol!cjO_Hzo+1Kshy%ujgn`-(DHyk`k?B-SvDL>fbz%8Lt;Dw%<*Dv}jZE^{5N zGj6KPx#Pa>!jb%4nSf^VhWpwjwZMMXK70Se}cz5*1e~iogA(Z=M zHyQQD?ZFwcfH0+>F*WUuGcx z8yQJ0f?{7TH*I70pxuqxG#$BhGZJjfbZ&k*-qQ177;WsJZ@~pI+U|mkaia+uLq-jm z2w@ z3^Ha&>~wPz8bGwpS+4js9LPL-eeW|_15k9G=elO!#u*M1*yepm-3n^i$qbTbp6M}t zci!kSpVsVd$Wm!E?hm76B(@x7OTsC4B=6bZl zSyFjpVT@iPCL`-{_P6=@prqAqwXP8OK=&#~bl%y7R)#$1B&Yatp=c0vAEm9&AB%Uk zdCbAM8`3(&S`&{ckdop91$E>2v%7RtyqfFSOz21$N*vbo4jv4UOW6NVc2T&&2ODHhfg_#?-WSCG-$GzyZ_!lspt477WJ=dA0EWZ z;MreT_CPD?8$X%u2T~NVi21291Nn|+zvx6}Eb**PjKeU>%RD8~qi4KHW!XfBmriZ(bfX>dWm>Lh zsju8L?@NCWD{f<039;1@8SeY&qv5t5n=sF6#5?ueb$IE*cNW(VjLDA1qWmx8mb%6@ z&tO+#TzN+)^h`Ob5J2Be4rq4vJN-{$N8A>S^)vHzQ2(2s0^E#srYZAz)8_dtk@?)C42 z$Xa7H-x1v1VwUQO+o&h3vZa!UJm+N^S81R;WD`TJn;Y*eK?0~(EZW9REc-h~Ei%RGeU9A^8Vo$FMHxXxnJnC1F#(7A;R>!Sj_d;HwC zK(IBq6h|K#cgCRcW{kHr&n{bdZRJgJx3YGPiyQ9A*fWC;`&M|UJ??J(t|OakqRahj z?A0n$ISMIR#+;j6`mu);0GjT$F6iL#b&Cs~xKQZePsdOZ&Kc!3GanZjZr= zTYf``o0|~IM}m8JusU0B0qYbpkCyWdU8U1#U*U+BE|XRXo9*B%4-2=JZ)GYz4|q=x zLFkkgpJ?~o3$ckp=79xYc>V_C#tXez@3qeuRtTgX2XzOGI?!8$MkCIBj2Xn+b7OZ# zDeHMy#vElfL|JcO?@PeZgSV~jhB<6~{Qj$@X2jGKux+i$dd`0PjOM2&t47*CE6eSt z!J>oKV2DB{3Cgc;t{B()-eq3MrUtJ!~S^d0_SZjae zV!m2lTWEa&ad`UP_|I!Lu_whS(9*CU_ZoKs$@%tx`-_%$p0xAST6^5vj~>%0AgCVR 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

5iDT2Mc7Om*`~Ifg^OnZi4c}nI12>1<*;Bq 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
cMCTrvkN`EbWlU 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Π&`t+pweS1|si1ujXt) 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%0Afny3-vu9~1XL`}ib+s+!tqD*0 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 literal 0 HcmV?d00001 diff --git a/.github/codex-cli-splash.png b/.github/codex-cli-splash.png new file mode 100644 index 0000000000000000000000000000000000000000..d3b27d78d0cd06a993b57b922a72a06a1bfe7c7e GIT binary patch literal 422175 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

5iDT2Mc7Om*`~Ifg^OnZi4c}nI12>1<*;Bq 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
cMCTrvkN`EbWlU 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

V<9l^AS2 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

*L7@{I3m(vccVZy?>0XOaQOlT$`iCN#i` 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 +

+ Codex CLI splash +

--- @@ -14,22 +15,27 @@ This is the home of the **Codex CLI**, which is a coding agent from OpenAI that -- [Experimental technology disclaimer](#experimental-technology-disclaimer) - [Quickstart](#quickstart) - - [OpenAI API Users](#openai-api-users) - - [OpenAI Plus/Pro Users](#openai-pluspro-users) + - [Installing and running Codex CLI](#installing-and-running-codex-cli) + - [Using Codex with your ChatGPT plan](#using-codex-with-your-chatgpt-plan) + - [Usage-based billing alternative: Use an OpenAI API key](#usage-based-billing-alternative-use-an-openai-api-key) + - [Choosing Codex's level of autonomy](#choosing-codexs-level-of-autonomy) + - [**1. Read/write**](#1-readwrite) + - [**2. Read-only**](#2-read-only) + - [**3. Advanced configuration**](#3-advanced-configuration) + - [Can I run without ANY approvals?](#can-i-run-without-any-approvals) + - [Fine-tuning in `config.toml`](#fine-tuning-in-configtoml) + - [Example prompts](#example-prompts) +- [Running with a prompt as input](#running-with-a-prompt-as-input) - [Using Open Source Models](#using-open-source-models) -- [Why Codex?](#why-codex) -- [Security model & permissions](#security-model--permissions) - [Platform sandboxing details](#platform-sandboxing-details) +- [Experimental technology disclaimer](#experimental-technology-disclaimer) - [System requirements](#system-requirements) - [CLI reference](#cli-reference) - [Memory & project docs](#memory--project-docs) - [Non-interactive / CI mode](#non-interactive--ci-mode) - [Model Context Protocol (MCP)](#model-context-protocol-mcp) - [Tracing / verbose logging](#tracing--verbose-logging) -- [Recipes](#recipes) -- [Installation](#installation) - [DotSlash](#dotslash) - [Configuration](#configuration) - [FAQ](#faq) @@ -54,55 +60,156 @@ This is the home of the **Codex CLI**, which is a coding agent from OpenAI that --- -## Experimental technology disclaimer - -Codex CLI is an experimental project under active development. It is not yet stable, may contain bugs, incomplete features, or undergo breaking changes. We're building it in the open with the community and welcome: - -- Bug reports -- Feature requests -- Pull requests -- Good vibes - -Help us improve by filing issues or submitting PRs (see the section below for how to contribute)! - ## Quickstart +### Installing and running Codex CLI + Install globally with your preferred package manager: ```shell npm install -g @openai/codex # Alternatively: `brew install codex` ``` -Or go to the [latest GitHub Release](https://github.com/openai/codex/releases/latest) and download the appropriate binary for your platform. +Then simply run `codex` to get started: -### OpenAI API Users +```shell +codex +``` -Next, set your OpenAI API key as an environment variable: +
+You can also go to the latest GitHub Release and download the appropriate binary for your platform. + +Each GitHub Release contains many executables, but in practice, you likely want one of these: + +- macOS + - Apple Silicon/arm64: `codex-aarch64-apple-darwin.tar.gz` + - x86_64 (older Mac hardware): `codex-x86_64-apple-darwin.tar.gz` +- Linux + - x86_64: `codex-x86_64-unknown-linux-musl.tar.gz` + - arm64: `codex-aarch64-unknown-linux-musl.tar.gz` + +Each archive contains a single entry with the platform baked into the name (e.g., `codex-x86_64-unknown-linux-musl`), so you likely want to rename it to `codex` after extracting it. + +
+ +### Using Codex with your ChatGPT plan + +

+ Codex CLI login +

+ +After you run `codex` select Sign in with ChatGPT. You'll need a Plus, Pro, or Team ChatGPT account, and will get access to our latest models, including `gpt-5`, at no extra cost to your plan. (Enterprise is coming soon.) + +> Important: If you've used the Codex CLI before, you'll need to follow these steps to migrate from usage-based billing with your API key: +> +> 1. Update the CLI with `codex update` and ensure `codex --version` is greater than 0.13 +> 2. Ensure that there is no `OPENAI_API_KEY` environment variable set. (Check that `env | grep 'OPENAI_API_KEY'` returns empty) +> 3. Run `codex login` again + +If you encounter problems with the login flow, please comment on [this issue](https://github.com/openai/codex/issues/1243). + +### Usage-based billing alternative: Use an OpenAI API key + +If you prefer to pay-as-you-go, you can still authenticate with your OpenAI API key by setting it as an environment variable: ```shell export OPENAI_API_KEY="your-api-key-here" ``` -> [!NOTE] -> This command sets the key only for your current terminal session. You can add the `export` line to your shell's configuration file (e.g., `~/.zshrc`), but we recommend setting it for the session. +> Note: This command only sets the key for your current terminal session, which we recommend. To set it for all future sessions, you can also add the `export` line to your shell's configuration file (e.g., `~/.zshrc`). -### OpenAI Plus/Pro Users +### Choosing Codex's level of autonomy -If you have a paid OpenAI account, run the following to start the login process: +We always recommend running Codex in its default sandbox that gives you strong guardrails around what the agent can do. The default sandbox prevents it from editing files outside its workspace, or from accessing the network. -``` -codex login +When you launch Codex in a new folder, it detects whether the folder is version controlled and recommends one of two levels of autonomy: + +#### **1. Read/write** + +- Codex can run commands and write files in the workspace without approval. +- To write files in other folders, access network, update git or perform other actions protected by the sandbox, Codex will need your permission. +- By default, the workspace includes the current directory, as well as temporary directories like `/tmp`. You can see what directories are in the workspace with the `/status` command. See the docs for how to customize this behavior. +- Advanced: You can manually specify this configuration by running `codex --sandbox workspace-write --ask-for-approval on-request` +- This is the recommended default for version-controlled folders. + +#### **2. Read-only** + +- Codex can run read-only commands without approval. +- To edit files, access network, or perform other actions protected by the sandbox, Codex will need your permission. +- Advanced: You can manually specify this configuration by running `codex --sandbox read-only --ask-for-approval on-request` +- This is the recommended default non-version-controlled folders. + +#### **3. Advanced configuration** + +Codex gives you fine-grained control over the sandbox with the `--sandbox` option, and over when it requests approval with the `--ask-for-approval` option. Run `codex help` for more on these options. + +#### Can I run without ANY approvals? + +Yes, run codex non-interactively with `--ask-for-approval never`. This option works with all `--sandbox` options, so you still have full control over Codex's level of autonomy. It will make its best attempt with whatever contrainsts you provide. For example: + +- Use `codex --ask-for-approval never --sandbox read-only` when you are running many agents to answer questions in parallel in the same workspace. +- Use `codex --ask-for-approval never --sandbox workspace-write` when you want the agent to non-interactively take time to produce the best outcome, with strong guardrails around its behavior. +- Use `codex --ask-for-approval never --sandbox danger-full-access` to dangerously give the agent full autonomy. Because this disables important safety mechanisms, we recommend against using this unless running Codex in an isolated environment. + +#### Fine-tuning in `config.toml` + +```toml +# approval mode +approval_policy = "untrusted" +sandbox_mode = "read-only" + +# full-auto mode +approval_policy = "on-request" +sandbox_mode = "workspace-write" + +# Optional: allow network in workspace-write mode +[sandbox_workspace_write] +network_access = true ``` -If you complete the process successfully, you should have a `~/.codex/auth.json` file that contains the credentials that Codex will use. +You can also save presets as **profiles**: -To verify whether you are currently logged in, run: +```toml +[profiles.full_auto] +approval_policy = "on-request" +sandbox_mode = "workspace-write" -``` -codex login status +[profiles.readonly_quiet] +approval_policy = "never" +sandbox_mode = "read-only" ``` -If you encounter problems with the login flow, please comment on . +### Example prompts + +Below are a few bite-size examples you can copy-paste. Replace the text in quotes with your own task. See the [prompting guide](https://github.com/openai/codex/blob/main/codex-cli/examples/prompting_guide.md) for more tips and usage patterns. + +| ✨ | What you type | What happens | +| --- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | +| 1 | `codex "Refactor the Dashboard component to React Hooks"` | Codex rewrites the class component, runs `npm test`, and shows the diff. | +| 2 | `codex "Generate SQL migrations for adding a users table"` | Infers your ORM, creates migration files, and runs them in a sandboxed DB. | +| 3 | `codex "Write unit tests for utils/date.ts"` | Generates tests, executes them, and iterates until they pass. | +| 4 | `codex "Bulk-rename *.jpeg -> *.jpg with git mv"` | Safely renames files and updates imports/usages. | +| 5 | `codex "Explain what this regex does: ^(?=.*[A-Z]).{8,}$"` | Outputs a step-by-step human explanation. | +| 6 | `codex "Carefully review this repo, and propose 3 high impact well-scoped PRs"` | Suggests impactful PRs in the current codebase. | +| 7 | `codex "Look for vulnerabilities and create a security review report"` | Finds and explains security bugs. | + +## Running with a prompt as input + +You can also run Codex CLI with a prompt as input: + +```shell +codex "explain this codebase to me" +``` + +```shell +codex --full-auto "create the fanciest todo-list app" +``` + +That's it - Codex will scaffold a file, run it inside a sandbox, install any +missing dependencies, and show you the live result. Approve the changes and +they'll be committed to your working directory. + +## Using Open Source Models
Use --profile to use other models @@ -163,31 +270,6 @@ model = "mistral" This way, you can specify one command-line argument (.e.g., `--profile o3`, `--profile mistral`) to override multiple settings together.
-
- -Run interactively: - -```shell -codex -``` - -Or, run with a prompt as input (and optionally in `Full Auto` mode): - -```shell -codex "explain this codebase to me" -``` - -```shell -codex --full-auto "create the fanciest todo-list app" -``` - -That's it - Codex will scaffold a file, run it inside a sandbox, install any -missing dependencies, and show you the live result. Approve the changes and -they'll be committed to your working directory. - ---- - -## Using Open Source Models Codex can run fully locally against an OpenAI-compatible OSS host (like Ollama) using the `--oss` flag: @@ -222,44 +304,6 @@ base_url = "http://my-ollama.example.com:11434/v1" --- -## Why Codex? - -Codex CLI is built for developers who already **live in the terminal** and want -ChatGPT-level reasoning **plus** the power to actually run code, manipulate -files, and iterate - all under version control. In short, it's _chat-driven -development_ that understands and executes your repo. - -- **Zero setup** - bring your OpenAI API key and it just works! -- **Full auto-approval, while safe + secure** by running network-disabled and directory-sandboxed -- **Multimodal** - pass in screenshots or diagrams to implement features ✨ - -And it's **fully open-source** so you can see and contribute to how it develops! - ---- - -## Security model & permissions - -Codex lets you decide _how much autonomy_ you want to grant the agent. The following options can be configured independently: - -- [`approval_policy`](./codex-rs/config.md#approval_policy) determines when you should be prompted to approve whether Codex can execute a command -- [`sandbox`](./codex-rs/config.md#sandbox) determines the _sandbox policy_ that Codex uses to execute untrusted commands - -By default, Codex runs with `--ask-for-approval untrusted` and `--sandbox read-only`, which means that: - -- The user is prompted to approve every command not on the set of "trusted" commands built into Codex (`cat`, `ls`, etc.) -- Approved commands are run outside of a sandbox because user approval implies "trust," in this case. - -Running Codex with the `--full-auto` convenience flag changes the configuration to `--ask-for-approval on-failure` and `--sandbox workspace-write`, which means that: - -- Codex does not initially ask for user approval before running an individual command. -- Though when it runs a command, it is run under a sandbox in which: - - It can read any file on the system. - - It can only write files under the current directory (or the directory specified via `--cd`). - - Network requests are completely disabled. -- Only if the command exits with a non-zero exit code will it ask the user for approval. If granted, it will re-attempt the command outside of the sandbox. (A common case is when Codex cannot `npm install` a dependency because that requires network access.) - -Again, these two options can be configured independently. For example, if you want Codex to perform an "exploration" where you are happy for it to read anything it wants but you never want to be prompted, you could run Codex with `--ask-for-approval never` and `--sandbox read-only`. - ### Platform sandboxing details The mechanism Codex uses to implement the sandbox policy depends on your OS: @@ -271,6 +315,19 @@ Note that when running Linux in a containerized environment such as Docker, sand --- +## Experimental technology disclaimer + +Codex CLI is an experimental project under active development. It is not yet stable, may contain bugs, incomplete features, or undergo breaking changes. We're building it in the open with the community and welcome: + +- Bug reports +- Feature requests +- Pull requests +- Good vibes + +Help us improve by filing issues or submitting PRs (see the section below for how to contribute)! + +--- + ## System requirements | Requirement | Details | @@ -346,52 +403,6 @@ See the Rust documentation on [`RUST_LOG`](https://docs.rs/env_logger/latest/env --- -## Recipes - -Below are a few bite-size examples you can copy-paste. Replace the text in quotes with your own task. See the [prompting guide](https://github.com/openai/codex/blob/main/codex-cli/examples/prompting_guide.md) for more tips and usage patterns. - -| ✨ | What you type | What happens | -| --- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -| 1 | `codex "Refactor the Dashboard component to React Hooks"` | Codex rewrites the class component, runs `npm test`, and shows the diff. | -| 2 | `codex "Generate SQL migrations for adding a users table"` | Infers your ORM, creates migration files, and runs them in a sandboxed DB. | -| 3 | `codex "Write unit tests for utils/date.ts"` | Generates tests, executes them, and iterates until they pass. | -| 4 | `codex "Bulk-rename *.jpeg -> *.jpg with git mv"` | Safely renames files and updates imports/usages. | -| 5 | `codex "Explain what this regex does: ^(?=.*[A-Z]).{8,}$"` | Outputs a step-by-step human explanation. | -| 6 | `codex "Carefully review this repo, and propose 3 high impact well-scoped PRs"` | Suggests impactful PRs in the current codebase. | -| 7 | `codex "Look for vulnerabilities and create a security review report"` | Finds and explains security bugs. | - ---- - -## Installation - -
-Install Codex CLI using your preferred package manager. - -From `brew` (recommended, downloads only the binary for your platform): - -```bash -brew install codex -``` - -From `npm` (generally more readily available, but downloads binaries for all supported platforms): - -```bash -npm i -g @openai/codex -``` - -Or go to the [latest GitHub Release](https://github.com/openai/codex/releases/latest) and download the appropriate binary for your platform. - -Admittedly, each GitHub Release contains many executables, but in practice, you likely want one of these: - -- macOS - - Apple Silicon/arm64: `codex-aarch64-apple-darwin.tar.gz` - - x86_64 (older Mac hardware): `codex-x86_64-apple-darwin.tar.gz` -- Linux - - x86_64: `codex-x86_64-unknown-linux-musl.tar.gz` - - arm64: `codex-aarch64-unknown-linux-musl.tar.gz` - -Each archive contains a single entry with the platform baked into the name (e.g., `codex-x86_64-unknown-linux-musl`), so you likely want to rename it to `codex` after extracting it. - ### DotSlash The GitHub Release also contains a [DotSlash](https://dotslash-cli.com/) file for the Codex CLI named `codex`. Using a DotSlash file makes it possible to make a lightweight commit to source control to ensure all contributors use the same version of an executable, regardless of what platform they use for development. From c78760381218a984a9f6c493f4fd52163d1fc666 Mon Sep 17 00:00:00 2001 From: Jeremy Rose <172423086+nornagon-openai@users.noreply.github.com> Date: Thu, 7 Aug 2025 14:27:44 -0400 Subject: [PATCH 0090/1309] ctrl+arrows also move words (#1949) this was removed at some point, but this is a common keybind for word left/right. --- codex-rs/tui/src/bottom_pane/textarea.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/textarea.rs b/codex-rs/tui/src/bottom_pane/textarea.rs index 8e6e8b07a3..b99087904a 100644 --- a/codex-rs/tui/src/bottom_pane/textarea.rs +++ b/codex-rs/tui/src/bottom_pane/textarea.rs @@ -300,14 +300,14 @@ impl TextArea { // Option/Right -> Alt+Right (next word end) KeyEvent { code: KeyCode::Left, - modifiers: KeyModifiers::ALT, + modifiers: KeyModifiers::ALT | KeyModifiers::CONTROL, .. } => { self.set_cursor(self.beginning_of_previous_word()); } KeyEvent { code: KeyCode::Right, - modifiers: KeyModifiers::ALT, + modifiers: KeyModifiers::ALT | KeyModifiers::CONTROL, .. } => { self.set_cursor(self.end_of_next_word()); From f74fe7af7b66714e4148fde6856bb6a7fac3eb7f Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 7 Aug 2025 13:11:06 -0700 Subject: [PATCH 0091/1309] fix: fix mistaken bitwise OR in #1949 (#1957) This is hard for me to test conclusively because I have the default of `ctrl left/right` used to migrate between Spaces on macOS. --- codex-rs/tui/src/bottom_pane/textarea.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/textarea.rs b/codex-rs/tui/src/bottom_pane/textarea.rs index b99087904a..c45c86e5af 100644 --- a/codex-rs/tui/src/bottom_pane/textarea.rs +++ b/codex-rs/tui/src/bottom_pane/textarea.rs @@ -300,14 +300,24 @@ impl TextArea { // Option/Right -> Alt+Right (next word end) KeyEvent { code: KeyCode::Left, - modifiers: KeyModifiers::ALT | KeyModifiers::CONTROL, + modifiers: KeyModifiers::ALT, + .. + } + | KeyEvent { + code: KeyCode::Left, + modifiers: KeyModifiers::CONTROL, .. } => { self.set_cursor(self.beginning_of_previous_word()); } KeyEvent { code: KeyCode::Right, - modifiers: KeyModifiers::ALT | KeyModifiers::CONTROL, + modifiers: KeyModifiers::ALT, + .. + } + | KeyEvent { + code: KeyCode::Right, + modifiers: KeyModifiers::CONTROL, .. } => { self.set_cursor(self.end_of_next_word()); From 7d67159587d7485ce0cddb6ed8d7433e8f195627 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 7 Aug 2025 14:19:30 -0700 Subject: [PATCH 0092/1309] fix: public load_auth() fn always called with include_env_var=true (#1961) Apparently `include_env_var=false` was only used for testing, so clean up the API a little to make that clear. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/1961). * #1962 * __->__ #1961 --- codex-rs/chatgpt/src/chatgpt_token.rs | 2 +- codex-rs/cli/src/login.rs | 2 +- codex-rs/cli/src/proto.rs | 2 +- codex-rs/core/src/codex_wrapper.rs | 2 +- codex-rs/login/src/lib.rs | 14 +++++++++----- codex-rs/tui/src/lib.rs | 2 +- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/codex-rs/chatgpt/src/chatgpt_token.rs b/codex-rs/chatgpt/src/chatgpt_token.rs index 55b6886c59..55ebc22a08 100644 --- a/codex-rs/chatgpt/src/chatgpt_token.rs +++ b/codex-rs/chatgpt/src/chatgpt_token.rs @@ -18,7 +18,7 @@ pub fn set_chatgpt_token_data(value: TokenData) { /// Initialize the ChatGPT token from auth.json file pub async fn init_chatgpt_token_from_auth(codex_home: &Path) -> std::io::Result<()> { - let auth = codex_login::load_auth(codex_home, true)?; + let auth = codex_login::load_auth(codex_home)?; if let Some(auth) = auth { let token_data = auth.get_token_data().await?; set_chatgpt_token_data(token_data); diff --git a/codex-rs/cli/src/login.rs b/codex-rs/cli/src/login.rs index 4291e06820..5f56eb1c4d 100644 --- a/codex-rs/cli/src/login.rs +++ b/codex-rs/cli/src/login.rs @@ -47,7 +47,7 @@ pub async fn run_login_with_api_key( pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! { let config = load_config_or_exit(cli_config_overrides); - match load_auth(&config.codex_home, true) { + match load_auth(&config.codex_home) { Ok(Some(auth)) => match auth.mode { AuthMode::ApiKey => { if let Some(api_key) = auth.api_key.as_deref() { diff --git a/codex-rs/cli/src/proto.rs b/codex-rs/cli/src/proto.rs index 9f9a94ed4d..291e1680f1 100644 --- a/codex-rs/cli/src/proto.rs +++ b/codex-rs/cli/src/proto.rs @@ -36,7 +36,7 @@ pub async fn run_main(opts: ProtoCli) -> anyhow::Result<()> { .map_err(anyhow::Error::msg)?; let config = Config::load_with_cli_overrides(overrides_vec, ConfigOverrides::default())?; - let auth = load_auth(&config.codex_home, true)?; + let auth = load_auth(&config.codex_home)?; let ctrl_c = notify_on_sigint(); let CodexSpawnOk { codex, .. } = Codex::spawn(config, auth, ctrl_c.clone()).await?; let codex = Arc::new(codex); diff --git a/codex-rs/core/src/codex_wrapper.rs b/codex-rs/core/src/codex_wrapper.rs index eeb4a7b470..1e26a9ebed 100644 --- a/codex-rs/core/src/codex_wrapper.rs +++ b/codex-rs/core/src/codex_wrapper.rs @@ -26,7 +26,7 @@ pub struct CodexConversation { /// that callers can surface the information to the UI. pub async fn init_codex(config: Config) -> anyhow::Result { let ctrl_c = notify_on_sigint(); - let auth = load_auth(&config.codex_home, true)?; + let auth = load_auth(&config.codex_home)?; let CodexSpawnOk { codex, init_id, diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index a52e105628..0157c77555 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -145,7 +145,11 @@ impl CodexAuth { } // Loads the available auth information from the auth.json or OPENAI_API_KEY environment variable. -pub fn load_auth(codex_home: &Path, include_env_var: bool) -> std::io::Result> { +pub fn load_auth(codex_home: &Path) -> std::io::Result> { + _load_auth(codex_home, true) +} + +fn _load_auth(codex_home: &Path, include_env_var: bool) -> std::io::Result> { let auth_file = get_auth_file(codex_home); let auth_dot_json = try_read_auth_json(&auth_file).ok(); @@ -421,7 +425,7 @@ mod tests { fn writes_api_key_and_loads_auth() { let dir = tempdir().unwrap(); login_with_api_key(dir.path(), "sk-test-key").unwrap(); - let auth = load_auth(dir.path(), false).unwrap().unwrap(); + let auth = _load_auth(dir.path(), false).unwrap().unwrap(); assert_eq!(auth.mode, AuthMode::ApiKey); assert_eq!(auth.api_key.as_deref(), Some("sk-test-key")); } @@ -434,7 +438,7 @@ mod tests { let env_var = std::env::var(OPENAI_API_KEY_ENV_VAR); if let Ok(env_var) = env_var { - let auth = load_auth(dir.path(), true).unwrap().unwrap(); + let auth = _load_auth(dir.path(), true).unwrap().unwrap(); assert_eq!(auth.mode, AuthMode::ApiKey); assert_eq!(auth.api_key, Some(env_var)); } @@ -493,7 +497,7 @@ mod tests { mode, auth_dot_json, auth_file, - } = load_auth(dir.path(), false).unwrap().unwrap(); + } = _load_auth(dir.path(), false).unwrap().unwrap(); assert_eq!(None, api_key); assert_eq!(AuthMode::ChatGPT, mode); assert_eq!(dir.path().join("auth.json"), auth_file); @@ -558,7 +562,7 @@ mod tests { ) .unwrap(); - let auth = load_auth(dir.path(), false).unwrap().unwrap(); + let auth = _load_auth(dir.path(), false).unwrap().unwrap(); assert_eq!(auth.mode, AuthMode::ApiKey); assert_eq!(auth.api_key, Some("sk-test-key".to_string())); diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 057d25168b..a5cfe28393 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -304,7 +304,7 @@ fn should_show_login_screen(config: &Config) -> bool { // Reading the OpenAI API key is an async operation because it may need // to refresh the token. Block on it. let codex_home = config.codex_home.clone(); - match load_auth(&codex_home, true) { + match load_auth(&codex_home) { Ok(Some(_)) => false, Ok(None) => true, Err(err) => { From 548466df0928bf031be13088096588a33d8de4a6 Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 7 Aug 2025 15:23:31 -0700 Subject: [PATCH 0093/1309] [client] Tune retries and backoff (#1956) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary 10 is a bit excessive 😅 Also updates our backoff factor to space out requests further. --- codex-rs/core/src/model_provider_info.rs | 2 +- codex-rs/core/src/util.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codex-rs/core/src/model_provider_info.rs b/codex-rs/core/src/model_provider_info.rs index a980211199..887917f470 100644 --- a/codex-rs/core/src/model_provider_info.rs +++ b/codex-rs/core/src/model_provider_info.rs @@ -15,7 +15,7 @@ use std::time::Duration; use crate::error::EnvVarError; const DEFAULT_STREAM_IDLE_TIMEOUT_MS: u64 = 300_000; -const DEFAULT_STREAM_MAX_RETRIES: u64 = 10; +const DEFAULT_STREAM_MAX_RETRIES: u64 = 5; const DEFAULT_REQUEST_MAX_RETRIES: u64 = 4; /// Wire protocol that the provider speaks. Most third-party services only diff --git a/codex-rs/core/src/util.rs b/codex-rs/core/src/util.rs index 5ba1e25666..fb5f45de6f 100644 --- a/codex-rs/core/src/util.rs +++ b/codex-rs/core/src/util.rs @@ -7,7 +7,7 @@ use tokio::sync::Notify; use tracing::debug; const INITIAL_DELAY_MS: u64 = 200; -const BACKOFF_FACTOR: f64 = 1.3; +const BACKOFF_FACTOR: f64 = 2.0; /// Make a CancellationToken that is fulfilled when SIGINT occurs. pub fn notify_on_sigint() -> Arc { From db76f3288876b78d65a1d502a8503a8e1bedf78b Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 7 Aug 2025 16:33:29 -0700 Subject: [PATCH 0094/1309] chore: rename CodexAuth::new() to create_dummy_codex_auth_for_testing() because it is not for general consumption (#1962) `CodexAuth::new()` was the first method listed in `CodexAuth`, but it is only meant to be used by tests. Rename it to `create_dummy_chatgpt_auth_for_testing()` and move it to the end of the implementation. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/1962). * #1971 * #1970 * #1966 * #1965 * __->__ #1962 --- codex-rs/core/tests/client.rs | 24 ++--------------------- codex-rs/login/src/lib.rs | 37 +++++++++++++++++++++-------------- 2 files changed, 24 insertions(+), 37 deletions(-) diff --git a/codex-rs/core/tests/client.rs b/codex-rs/core/tests/client.rs index 2148e874bc..2ea772e3b7 100644 --- a/codex-rs/core/tests/client.rs +++ b/codex-rs/core/tests/client.rs @@ -1,8 +1,5 @@ -#![allow(clippy::expect_used)] -#![allow(clippy::unwrap_used)] -use std::path::PathBuf; +#![allow(clippy::expect_used, clippy::unwrap_used)] -use chrono::Utc; use codex_core::Codex; use codex_core::CodexSpawnOk; use codex_core::ModelProviderInfo; @@ -13,10 +10,7 @@ use codex_core::protocol::InputItem; use codex_core::protocol::Op; use codex_core::protocol::SessionConfiguredEvent; use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; -use codex_login::AuthDotJson; -use codex_login::AuthMode; use codex_login::CodexAuth; -use codex_login::TokenData; use core_test_support::load_default_config_for_test; use core_test_support::load_sse_fixture_with_id; use core_test_support::wait_for_event; @@ -556,19 +550,5 @@ async fn env_var_overrides_loaded_auth() { } fn create_dummy_codex_auth() -> CodexAuth { - CodexAuth::new( - None, - AuthMode::ChatGPT, - PathBuf::new(), - Some(AuthDotJson { - openai_api_key: None, - tokens: Some(TokenData { - id_token: Default::default(), - access_token: "Access Token".to_string(), - refresh_token: "test".to_string(), - account_id: Some("account_id".to_string()), - }), - last_refresh: Some(Utc::now()), - }), - ) + CodexAuth::create_dummy_chatgpt_auth_for_testing() } diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index 0157c77555..3aa1816fcb 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -51,21 +51,6 @@ impl PartialEq for CodexAuth { } impl CodexAuth { - pub fn new( - api_key: Option, - mode: AuthMode, - auth_file: PathBuf, - auth_dot_json: Option, - ) -> Self { - let auth_dot_json = Arc::new(Mutex::new(auth_dot_json)); - Self { - api_key, - mode, - auth_file, - auth_dot_json, - } - } - pub fn from_api_key(api_key: String) -> Self { Self { api_key: Some(api_key), @@ -142,6 +127,28 @@ impl CodexAuth { } } } + + /// Consider this private to integration tests. + pub fn create_dummy_chatgpt_auth_for_testing() -> Self { + let auth_dot_json = AuthDotJson { + openai_api_key: None, + tokens: Some(TokenData { + id_token: Default::default(), + access_token: "Access Token".to_string(), + refresh_token: "test".to_string(), + account_id: Some("account_id".to_string()), + }), + last_refresh: Some(Utc::now()), + }; + + let auth_dot_json = Arc::new(Mutex::new(Some(auth_dot_json))); + Self { + api_key: None, + mode: AuthMode::ChatGPT, + auth_file: PathBuf::new(), + auth_dot_json, + } + } } // Loads the available auth information from the auth.json or OPENAI_API_KEY environment variable. From 02c9c2ecad78ba34e5f6a88f7f2f65582e795114 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 7 Aug 2025 16:40:01 -0700 Subject: [PATCH 0095/1309] chore: make CodexAuth::api_key a private field (#1965) Force callers to access this information via `get_token()` rather than messing with it directly. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/1965). * #1971 * #1970 * #1966 * __->__ #1965 * #1962 --- codex-rs/cli/src/login.rs | 16 +++++++++------- codex-rs/login/src/lib.rs | 3 ++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/codex-rs/cli/src/login.rs b/codex-rs/cli/src/login.rs index 5f56eb1c4d..3881a1ce3f 100644 --- a/codex-rs/cli/src/login.rs +++ b/codex-rs/cli/src/login.rs @@ -49,9 +49,9 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! { match load_auth(&config.codex_home) { Ok(Some(auth)) => match auth.mode { - AuthMode::ApiKey => { - if let Some(api_key) = auth.api_key.as_deref() { - eprintln!("Logged in using an API key - {}", safe_format_key(api_key)); + AuthMode::ApiKey => match auth.get_token().await { + Ok(api_key) => { + eprintln!("Logged in using an API key - {}", safe_format_key(&api_key)); if let Ok(env_api_key) = env::var(OPENAI_API_KEY_ENV_VAR) { if env_api_key == api_key { @@ -60,11 +60,13 @@ pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! { ); } } - } else { - eprintln!("Logged in using an API key"); + std::process::exit(0); } - std::process::exit(0); - } + Err(e) => { + eprintln!("Unexpected error retrieving API key: {e}"); + std::process::exit(1); + } + }, AuthMode::ChatGPT => { eprintln!("Logged in using ChatGPT"); std::process::exit(0); diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index 3aa1816fcb..36a78aaa22 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -38,8 +38,9 @@ pub enum AuthMode { #[derive(Debug, Clone)] pub struct CodexAuth { - pub api_key: Option, pub mode: AuthMode, + + api_key: Option, auth_dot_json: Arc>>, auth_file: PathBuf, } From b991c04f86f81c8ac2b41187af20572338e2ee12 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 7 Aug 2025 16:49:37 -0700 Subject: [PATCH 0096/1309] chore: move top-level load_auth() to CodexAuth::from_codex_home() (#1966) There are two valid ways to create an instance of `CodexAuth`: `from_api_key()` and `from_codex_home()`. Now both are static methods of `CodexAuth` and are listed first in the implementation. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/1966). * #1971 * #1970 * __->__ #1966 * #1965 * #1962 --- codex-rs/chatgpt/src/chatgpt_token.rs | 3 ++- codex-rs/cli/src/login.rs | 4 ++-- codex-rs/cli/src/proto.rs | 4 ++-- codex-rs/core/src/codex_wrapper.rs | 4 ++-- codex-rs/login/src/lib.rs | 21 +++++++++++---------- codex-rs/tui/src/lib.rs | 4 ++-- 6 files changed, 21 insertions(+), 19 deletions(-) diff --git a/codex-rs/chatgpt/src/chatgpt_token.rs b/codex-rs/chatgpt/src/chatgpt_token.rs index 55ebc22a08..c674afbc57 100644 --- a/codex-rs/chatgpt/src/chatgpt_token.rs +++ b/codex-rs/chatgpt/src/chatgpt_token.rs @@ -1,3 +1,4 @@ +use codex_login::CodexAuth; use std::path::Path; use std::sync::LazyLock; use std::sync::RwLock; @@ -18,7 +19,7 @@ pub fn set_chatgpt_token_data(value: TokenData) { /// Initialize the ChatGPT token from auth.json file pub async fn init_chatgpt_token_from_auth(codex_home: &Path) -> std::io::Result<()> { - let auth = codex_login::load_auth(codex_home)?; + let auth = CodexAuth::from_codex_home(codex_home)?; if let Some(auth) = auth { let token_data = auth.get_token_data().await?; set_chatgpt_token_data(token_data); diff --git a/codex-rs/cli/src/login.rs b/codex-rs/cli/src/login.rs index 3881a1ce3f..1a70bd27b6 100644 --- a/codex-rs/cli/src/login.rs +++ b/codex-rs/cli/src/login.rs @@ -4,8 +4,8 @@ use codex_common::CliConfigOverrides; use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_login::AuthMode; +use codex_login::CodexAuth; use codex_login::OPENAI_API_KEY_ENV_VAR; -use codex_login::load_auth; use codex_login::login_with_api_key; use codex_login::login_with_chatgpt; use codex_login::logout; @@ -47,7 +47,7 @@ pub async fn run_login_with_api_key( pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! { let config = load_config_or_exit(cli_config_overrides); - match load_auth(&config.codex_home) { + match CodexAuth::from_codex_home(&config.codex_home) { Ok(Some(auth)) => match auth.mode { AuthMode::ApiKey => match auth.get_token().await { Ok(api_key) => { diff --git a/codex-rs/cli/src/proto.rs b/codex-rs/cli/src/proto.rs index 291e1680f1..6c1de7eaa9 100644 --- a/codex-rs/cli/src/proto.rs +++ b/codex-rs/cli/src/proto.rs @@ -9,7 +9,7 @@ use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_core::protocol::Submission; use codex_core::util::notify_on_sigint; -use codex_login::load_auth; +use codex_login::CodexAuth; use tokio::io::AsyncBufReadExt; use tokio::io::BufReader; use tracing::error; @@ -36,7 +36,7 @@ pub async fn run_main(opts: ProtoCli) -> anyhow::Result<()> { .map_err(anyhow::Error::msg)?; let config = Config::load_with_cli_overrides(overrides_vec, ConfigOverrides::default())?; - let auth = load_auth(&config.codex_home)?; + let auth = CodexAuth::from_codex_home(&config.codex_home)?; let ctrl_c = notify_on_sigint(); let CodexSpawnOk { codex, .. } = Codex::spawn(config, auth, ctrl_c.clone()).await?; let codex = Arc::new(codex); diff --git a/codex-rs/core/src/codex_wrapper.rs b/codex-rs/core/src/codex_wrapper.rs index 1e26a9ebed..dc10ec8d84 100644 --- a/codex-rs/core/src/codex_wrapper.rs +++ b/codex-rs/core/src/codex_wrapper.rs @@ -6,7 +6,7 @@ use crate::config::Config; use crate::protocol::Event; use crate::protocol::EventMsg; use crate::util::notify_on_sigint; -use codex_login::load_auth; +use codex_login::CodexAuth; use tokio::sync::Notify; use uuid::Uuid; @@ -26,7 +26,7 @@ pub struct CodexConversation { /// that callers can surface the information to the UI. pub async fn init_codex(config: Config) -> anyhow::Result { let ctrl_c = notify_on_sigint(); - let auth = load_auth(&config.codex_home)?; + let auth = CodexAuth::from_codex_home(&config.codex_home)?; let CodexSpawnOk { codex, init_id, diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index 36a78aaa22..8c8ad75bbc 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -61,6 +61,12 @@ impl CodexAuth { } } + /// Loads the available auth information from the auth.json or + /// OPENAI_API_KEY environment variable. + pub fn from_codex_home(codex_home: &Path) -> std::io::Result> { + load_auth(codex_home, true) + } + pub async fn get_token_data(&self) -> Result { #[expect(clippy::unwrap_used)] let auth_dot_json = self.auth_dot_json.lock().unwrap().clone(); @@ -152,12 +158,7 @@ impl CodexAuth { } } -// Loads the available auth information from the auth.json or OPENAI_API_KEY environment variable. -pub fn load_auth(codex_home: &Path) -> std::io::Result> { - _load_auth(codex_home, true) -} - -fn _load_auth(codex_home: &Path, include_env_var: bool) -> std::io::Result> { +fn load_auth(codex_home: &Path, include_env_var: bool) -> std::io::Result> { let auth_file = get_auth_file(codex_home); let auth_dot_json = try_read_auth_json(&auth_file).ok(); @@ -433,7 +434,7 @@ mod tests { fn writes_api_key_and_loads_auth() { let dir = tempdir().unwrap(); login_with_api_key(dir.path(), "sk-test-key").unwrap(); - let auth = _load_auth(dir.path(), false).unwrap().unwrap(); + let auth = load_auth(dir.path(), false).unwrap().unwrap(); assert_eq!(auth.mode, AuthMode::ApiKey); assert_eq!(auth.api_key.as_deref(), Some("sk-test-key")); } @@ -446,7 +447,7 @@ mod tests { let env_var = std::env::var(OPENAI_API_KEY_ENV_VAR); if let Ok(env_var) = env_var { - let auth = _load_auth(dir.path(), true).unwrap().unwrap(); + let auth = load_auth(dir.path(), true).unwrap().unwrap(); assert_eq!(auth.mode, AuthMode::ApiKey); assert_eq!(auth.api_key, Some(env_var)); } @@ -505,7 +506,7 @@ mod tests { mode, auth_dot_json, auth_file, - } = _load_auth(dir.path(), false).unwrap().unwrap(); + } = load_auth(dir.path(), false).unwrap().unwrap(); assert_eq!(None, api_key); assert_eq!(AuthMode::ChatGPT, mode); assert_eq!(dir.path().join("auth.json"), auth_file); @@ -570,7 +571,7 @@ mod tests { ) .unwrap(); - let auth = _load_auth(dir.path(), false).unwrap().unwrap(); + let auth = load_auth(dir.path(), false).unwrap().unwrap(); assert_eq!(auth.mode, AuthMode::ApiKey); assert_eq!(auth.api_key, Some("sk-test-key".to_string())); diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index a5cfe28393..e15a235a71 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -12,7 +12,7 @@ use codex_core::config::load_config_as_toml_with_cli_overrides; use codex_core::config_types::SandboxMode; use codex_core::protocol::AskForApproval; use codex_core::protocol::SandboxPolicy; -use codex_login::load_auth; +use codex_login::CodexAuth; use codex_ollama::DEFAULT_OSS_MODEL; use log_layer::TuiLogLayer; use std::fs::OpenOptions; @@ -304,7 +304,7 @@ fn should_show_login_screen(config: &Config) -> bool { // Reading the OpenAI API key is an async operation because it may need // to refresh the token. Block on it. let codex_home = config.codex_home.clone(); - match load_auth(&codex_home) { + match CodexAuth::from_codex_home(&codex_home) { Ok(Some(_)) => false, Ok(None) => true, Err(err) => { From 295abf3e511895bfe860382d4f84a15bbca2ef8c Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 7 Aug 2025 16:55:33 -0700 Subject: [PATCH 0097/1309] chore: change CodexAuth::from_api_key() to take &str instead of String (#1970) Good practice and simplifies some of the call sites. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/1970). * #1971 * __->__ #1970 * #1966 * #1965 * #1962 --- codex-rs/core/src/model_provider_info.rs | 2 +- codex-rs/core/tests/client.rs | 8 ++++---- codex-rs/core/tests/compact.rs | 2 +- codex-rs/core/tests/stream_no_completed.rs | 2 +- codex-rs/login/src/lib.rs | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/codex-rs/core/src/model_provider_info.rs b/codex-rs/core/src/model_provider_info.rs index 887917f470..98f07deb1e 100644 --- a/codex-rs/core/src/model_provider_info.rs +++ b/codex-rs/core/src/model_provider_info.rs @@ -96,7 +96,7 @@ impl ModelProviderInfo { auth: &Option, ) -> crate::error::Result { let effective_auth = match self.api_key() { - Ok(Some(key)) => Some(CodexAuth::from_api_key(key)), + Ok(Some(key)) => Some(CodexAuth::from_api_key(&key)), Ok(None) => auth.clone(), Err(err) => { if auth.is_some() { diff --git a/codex-rs/core/tests/client.rs b/codex-rs/core/tests/client.rs index 2ea772e3b7..bd7ad2feef 100644 --- a/codex-rs/core/tests/client.rs +++ b/codex-rs/core/tests/client.rs @@ -93,7 +93,7 @@ async fn includes_session_id_and_model_headers_in_request() { let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new()); let CodexSpawnOk { codex, .. } = Codex::spawn( config, - Some(CodexAuth::from_api_key("Test API Key".to_string())), + Some(CodexAuth::from_api_key("Test API Key")), ctrl_c.clone(), ) .await @@ -167,7 +167,7 @@ async fn includes_base_instructions_override_in_request() { let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new()); let CodexSpawnOk { codex, .. } = Codex::spawn( config, - Some(CodexAuth::from_api_key("Test API Key".to_string())), + Some(CodexAuth::from_api_key("Test API Key")), ctrl_c.clone(), ) .await @@ -226,7 +226,7 @@ async fn originator_config_override_is_used() { let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new()); let CodexSpawnOk { codex, .. } = Codex::spawn( config, - Some(CodexAuth::from_api_key("Test API Key".to_string())), + Some(CodexAuth::from_api_key("Test API Key")), ctrl_c.clone(), ) .await @@ -364,7 +364,7 @@ async fn includes_user_instructions_message_in_request() { let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new()); let CodexSpawnOk { codex, .. } = Codex::spawn( config, - Some(CodexAuth::from_api_key("Test API Key".to_string())), + Some(CodexAuth::from_api_key("Test API Key")), ctrl_c.clone(), ) .await diff --git a/codex-rs/core/tests/compact.rs b/codex-rs/core/tests/compact.rs index bf47feed1f..fa5c81d883 100644 --- a/codex-rs/core/tests/compact.rs +++ b/codex-rs/core/tests/compact.rs @@ -145,7 +145,7 @@ async fn summarize_context_three_requests_and_instructions() { let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new()); let CodexSpawnOk { codex, .. } = Codex::spawn( config, - Some(CodexAuth::from_api_key("dummy".to_string())), + Some(CodexAuth::from_api_key("dummy")), ctrl_c.clone(), ) .await diff --git a/codex-rs/core/tests/stream_no_completed.rs b/codex-rs/core/tests/stream_no_completed.rs index 8a4216b129..0ded3337ab 100644 --- a/codex-rs/core/tests/stream_no_completed.rs +++ b/codex-rs/core/tests/stream_no_completed.rs @@ -99,7 +99,7 @@ async fn retries_on_early_close() { config.model_provider = model_provider; let CodexSpawnOk { codex, .. } = Codex::spawn( config, - Some(CodexAuth::from_api_key("Test API Key".to_string())), + Some(CodexAuth::from_api_key("Test API Key")), ctrl_c, ) .await diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index 8c8ad75bbc..8571abcfb6 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -52,9 +52,9 @@ impl PartialEq for CodexAuth { } impl CodexAuth { - pub fn from_api_key(api_key: String) -> Self { + pub fn from_api_key(api_key: &str) -> Self { Self { - api_key: Some(api_key), + api_key: Some(api_key.to_owned()), mode: AuthMode::ApiKey, auth_file: PathBuf::new(), auth_dot_json: Arc::new(Mutex::new(None)), From cd06b28d8466349b79bea098e05fd4e1d22972c0 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 7 Aug 2025 18:00:31 -0700 Subject: [PATCH 0098/1309] fix: default to credits from ChatGPT auth, when possible (#1971) Uses this rough strategy for authentication: ``` if auth.json if auth.json.API_KEY is NULL # new auth CHAT else # old auth if plus or pro or team CHAT else API_KEY else OPENAI_API_KEY ``` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/1970). * __->__ #1971 * #1970 * #1966 * #1965 * #1962 --- codex-rs/login/src/lib.rs | 293 +++++++++++++++++++++---------- codex-rs/login/src/token_data.rs | 63 ++++++- codex-rs/tui/src/history_cell.rs | 9 +- 3 files changed, 268 insertions(+), 97 deletions(-) diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index 8571abcfb6..7e693ccdf8 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -159,47 +159,77 @@ impl CodexAuth { } fn load_auth(codex_home: &Path, include_env_var: bool) -> std::io::Result> { + // First, check to see if there is a valid auth.json file. If not, we fall + // back to AuthMode::ApiKey using the OPENAI_API_KEY environment variable + // (if it is set). let auth_file = get_auth_file(codex_home); - - let auth_dot_json = try_read_auth_json(&auth_file).ok(); - - let auth_json_api_key = auth_dot_json - .as_ref() - .and_then(|a| a.openai_api_key.clone()) - .filter(|s| !s.is_empty()); - - let openai_api_key = if include_env_var { - env::var(OPENAI_API_KEY_ENV_VAR) - .ok() - .filter(|s| !s.is_empty()) - .or(auth_json_api_key) - } else { - auth_json_api_key + let auth_dot_json = match try_read_auth_json(&auth_file) { + Ok(auth) => auth, + // If auth.json does not exist, try to read the OPENAI_API_KEY from the + // environment variable. + Err(e) if e.kind() == std::io::ErrorKind::NotFound && include_env_var => { + return match read_openai_api_key_from_env() { + Some(api_key) => Ok(Some(CodexAuth::from_api_key(&api_key))), + None => Ok(None), + }; + } + // Though if auth.json exists but is malformed, do not fall back to the + // env var because the user may be expecting to use AuthMode::ChatGPT. + Err(e) => { + return Err(e); + } }; - let has_tokens = auth_dot_json - .as_ref() - .and_then(|a| a.tokens.as_ref()) - .is_some(); + let AuthDotJson { + openai_api_key: auth_json_api_key, + tokens, + last_refresh, + } = auth_dot_json; - if openai_api_key.is_none() && !has_tokens { - return Ok(None); + // If the auth.json has an API key AND does not appear to be on a plan that + // should prefer AuthMode::ChatGPT, use AuthMode::ApiKey. + if let Some(api_key) = &auth_json_api_key { + // Should any of these be AuthMode::ChatGPT with the api_key set? + // Does AuthMode::ChatGPT indicate that there is an auth.json that is + // "refreshable" even if we are using the API key for auth? + match &tokens { + Some(tokens) => { + if tokens.is_plan_that_should_use_api_key() { + return Ok(Some(CodexAuth::from_api_key(api_key))); + } else { + // Ignore the API key and fall through to ChatGPT auth. + } + } + None => { + // We have an API key but no tokens in the auth.json file. + // Perhaps the user ran `codex login --api-key ` or updated + // auth.json by hand. Either way, let's assume they are trying + // to use their API key. + return Ok(Some(CodexAuth::from_api_key(api_key))); + } + } } - let mode = if openai_api_key.is_some() { - AuthMode::ApiKey - } else { - AuthMode::ChatGPT - }; - + // For the AuthMode::ChatGPT variant, perhaps neither api_key nor + // openai_api_key should exist? Ok(Some(CodexAuth { - api_key: openai_api_key, - mode, + api_key: None, + mode: AuthMode::ChatGPT, auth_file, - auth_dot_json: Arc::new(Mutex::new(auth_dot_json)), + auth_dot_json: Arc::new(Mutex::new(Some(AuthDotJson { + openai_api_key: None, + tokens, + last_refresh, + }))), })) } +fn read_openai_api_key_from_env() -> Option { + env::var(OPENAI_API_KEY_ENV_VAR) + .ok() + .filter(|s| !s.is_empty()) +} + pub fn get_auth_file(codex_home: &Path) -> PathBuf { codex_home.join("auth.json") } @@ -423,14 +453,19 @@ pub struct AuthDotJson { #[cfg(test)] mod tests { + #![expect(clippy::expect_used, clippy::unwrap_used)] use super::*; use crate::token_data::IdTokenInfo; + use crate::token_data::KnownPlan; + use crate::token_data::PlanType; use base64::Engine; use pretty_assertions::assert_eq; + use serde_json::json; use tempfile::tempdir; + const LAST_REFRESH: &str = "2025-08-06T20:41:36.232376Z"; + #[test] - #[expect(clippy::unwrap_used)] fn writes_api_key_and_loads_auth() { let dir = tempdir().unwrap(); login_with_api_key(dir.path(), "sk-test-key").unwrap(); @@ -440,7 +475,6 @@ mod tests { } #[test] - #[expect(clippy::unwrap_used)] fn loads_from_env_var_if_env_var_exists() { let dir = tempdir().unwrap(); @@ -454,10 +488,132 @@ mod tests { } #[tokio::test] - #[expect(clippy::expect_used, clippy::unwrap_used)] - async fn loads_token_data_from_auth_json() { - let dir = tempdir().unwrap(); - let auth_file = dir.path().join("auth.json"); + async fn pro_account_with_no_api_key_uses_chatgpt_auth() { + let codex_home = tempdir().unwrap(); + write_auth_file( + AuthFileParams { + openai_api_key: None, + chatgpt_plan_type: "pro".to_string(), + }, + codex_home.path(), + ) + .expect("failed to write auth file"); + + let CodexAuth { + api_key, + mode, + auth_dot_json, + auth_file: _, + } = load_auth(codex_home.path(), false).unwrap().unwrap(); + assert_eq!(None, api_key); + assert_eq!(AuthMode::ChatGPT, mode); + + let guard = auth_dot_json.lock().unwrap(); + let auth_dot_json = guard.as_ref().expect("AuthDotJson should exist"); + assert_eq!( + &AuthDotJson { + openai_api_key: None, + tokens: Some(TokenData { + id_token: IdTokenInfo { + email: Some("user@example.com".to_string()), + chatgpt_plan_type: Some(PlanType::Known(KnownPlan::Pro)), + }, + access_token: "test-access-token".to_string(), + refresh_token: "test-refresh-token".to_string(), + account_id: None, + }), + last_refresh: Some( + DateTime::parse_from_rfc3339(LAST_REFRESH) + .unwrap() + .with_timezone(&Utc) + ), + }, + auth_dot_json + ) + } + + /// Even if the OPENAI_API_KEY is set in auth.json, if the plan is not in + /// [`TokenData::is_plan_that_should_use_api_key`], it should use + /// [`AuthMode::ChatGPT`]. + #[tokio::test] + async fn pro_account_with_api_key_still_uses_chatgpt_auth() { + let codex_home = tempdir().unwrap(); + write_auth_file( + AuthFileParams { + openai_api_key: Some("sk-test-key".to_string()), + chatgpt_plan_type: "pro".to_string(), + }, + codex_home.path(), + ) + .expect("failed to write auth file"); + + let CodexAuth { + api_key, + mode, + auth_dot_json, + auth_file: _, + } = load_auth(codex_home.path(), false).unwrap().unwrap(); + assert_eq!(None, api_key); + assert_eq!(AuthMode::ChatGPT, mode); + + let guard = auth_dot_json.lock().unwrap(); + let auth_dot_json = guard.as_ref().expect("AuthDotJson should exist"); + assert_eq!( + &AuthDotJson { + openai_api_key: None, + tokens: Some(TokenData { + id_token: IdTokenInfo { + email: Some("user@example.com".to_string()), + chatgpt_plan_type: Some(PlanType::Known(KnownPlan::Pro)), + }, + access_token: "test-access-token".to_string(), + refresh_token: "test-refresh-token".to_string(), + account_id: None, + }), + last_refresh: Some( + DateTime::parse_from_rfc3339(LAST_REFRESH) + .unwrap() + .with_timezone(&Utc) + ), + }, + auth_dot_json + ) + } + + /// If the OPENAI_API_KEY is set in auth.json and it is an enterprise + /// account, then it should use [`AuthMode::ApiKey`]. + #[tokio::test] + async fn enterprise_account_with_api_key_uses_chatgpt_auth() { + let codex_home = tempdir().unwrap(); + write_auth_file( + AuthFileParams { + openai_api_key: Some("sk-test-key".to_string()), + chatgpt_plan_type: "enterprise".to_string(), + }, + codex_home.path(), + ) + .expect("failed to write auth file"); + + let CodexAuth { + api_key, + mode, + auth_dot_json, + auth_file: _, + } = load_auth(codex_home.path(), false).unwrap().unwrap(); + assert_eq!(Some("sk-test-key".to_string()), api_key); + assert_eq!(AuthMode::ApiKey, mode); + + let guard = auth_dot_json.lock().expect("should unwrap"); + assert!(guard.is_none(), "auth_dot_json should be None"); + } + + struct AuthFileParams { + openai_api_key: Option, + chatgpt_plan_type: String, + } + + fn write_auth_file(params: AuthFileParams, codex_home: &Path) -> std::io::Result<()> { + let auth_file = get_auth_file(codex_home); // Create a minimal valid JWT for the id_token field. #[derive(Serialize)] struct Header { @@ -473,71 +629,31 @@ mod tests { "email_verified": true, "https://api.openai.com/auth": { "chatgpt_account_id": "bc3618e3-489d-4d49-9362-1561dc53ba53", - "chatgpt_plan_type": "pro", + "chatgpt_plan_type": params.chatgpt_plan_type, "chatgpt_user_id": "user-12345", "user_id": "user-12345", } }); let b64 = |b: &[u8]| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b); - let header_b64 = b64(&serde_json::to_vec(&header).unwrap()); - let payload_b64 = b64(&serde_json::to_vec(&payload).unwrap()); + let header_b64 = b64(&serde_json::to_vec(&header)?); + let payload_b64 = b64(&serde_json::to_vec(&payload)?); let signature_b64 = b64(b"sig"); let fake_jwt = format!("{header_b64}.{payload_b64}.{signature_b64}"); - std::fs::write( - auth_file, - format!( - r#" - {{ - "OPENAI_API_KEY": null, - "tokens": {{ - "id_token": "{fake_jwt}", + + let auth_json_data = json!({ + "OPENAI_API_KEY": params.openai_api_key, + "tokens": { + "id_token": fake_jwt, "access_token": "test-access-token", "refresh_token": "test-refresh-token" - }}, - "last_refresh": "2025-08-06T20:41:36.232376Z" - }} - "#, - ), - ) - .unwrap(); - - let CodexAuth { - api_key, - mode, - auth_dot_json, - auth_file, - } = load_auth(dir.path(), false).unwrap().unwrap(); - assert_eq!(None, api_key); - assert_eq!(AuthMode::ChatGPT, mode); - assert_eq!(dir.path().join("auth.json"), auth_file); - - let guard = auth_dot_json.lock().unwrap(); - let auth_dot_json = guard.as_ref().expect("AuthDotJson should exist"); - - assert_eq!( - &AuthDotJson { - openai_api_key: None, - tokens: Some(TokenData { - id_token: IdTokenInfo { - email: Some("user@example.com".to_string()), - chatgpt_plan_type: Some("pro".to_string()), - }, - access_token: "test-access-token".to_string(), - refresh_token: "test-refresh-token".to_string(), - account_id: None, - }), - last_refresh: Some( - DateTime::parse_from_rfc3339("2025-08-06T20:41:36.232376Z") - .unwrap() - .with_timezone(&Utc) - ), }, - auth_dot_json - ) + "last_refresh": LAST_REFRESH, + }); + let auth_json = serde_json::to_string_pretty(&auth_json_data)?; + std::fs::write(auth_file, auth_json) } #[test] - #[expect(clippy::expect_used, clippy::unwrap_used)] fn id_token_info_handles_missing_fields() { // Payload without email or plan should yield None values. let header = serde_json::json!({"alg": "none", "typ": "JWT"}); @@ -555,7 +671,6 @@ mod tests { } #[tokio::test] - #[expect(clippy::unwrap_used)] async fn loads_api_key_from_auth_json() { let dir = tempdir().unwrap(); let auth_file = dir.path().join("auth.json"); diff --git a/codex-rs/login/src/token_data.rs b/codex-rs/login/src/token_data.rs index 55b51b9d44..86ddaf5819 100644 --- a/codex-rs/login/src/token_data.rs +++ b/codex-rs/login/src/token_data.rs @@ -17,6 +17,17 @@ pub struct TokenData { pub account_id: Option, } +impl TokenData { + /// Returns true if this is a plan that should use the traditional + /// "metered" billing via an API key. + pub(crate) fn is_plan_that_should_use_api_key(&self) -> bool { + self.id_token + .chatgpt_plan_type + .as_ref() + .is_none_or(|plan| plan.is_plan_that_should_use_api_key()) + } +} + /// Flat subset of useful claims in id_token from auth.json. #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)] pub struct IdTokenInfo { @@ -24,7 +35,50 @@ pub struct IdTokenInfo { /// The ChatGPT subscription plan type /// (e.g., "free", "plus", "pro", "business", "enterprise", "edu"). /// (Note: ae has not verified that those are the exact values.) - pub chatgpt_plan_type: Option, + pub(crate) chatgpt_plan_type: Option, +} + +impl IdTokenInfo { + pub fn get_chatgpt_plan_type(&self) -> Option { + self.chatgpt_plan_type.as_ref().map(|t| match t { + PlanType::Known(plan) => format!("{plan:?}"), + PlanType::Unknown(s) => s.clone(), + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged)] +pub(crate) enum PlanType { + Known(KnownPlan), + Unknown(String), +} + +impl PlanType { + fn is_plan_that_should_use_api_key(&self) -> bool { + match self { + Self::Known(known) => { + use KnownPlan::*; + !matches!(known, Free | Plus | Pro | Team) + } + Self::Unknown(_) => { + // Unknown plans should use the API key. + true + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub(crate) enum KnownPlan { + Free, + Plus, + Pro, + Team, + Business, + Enterprise, + Edu, } #[derive(Deserialize)] @@ -38,7 +92,7 @@ struct IdClaims { #[derive(Deserialize)] struct AuthClaims { #[serde(default)] - chatgpt_plan_type: Option, + chatgpt_plan_type: Option, } #[derive(Debug, Error)] @@ -112,6 +166,9 @@ mod tests { let info = parse_id_token(&fake_jwt).expect("should parse"); assert_eq!(info.email.as_deref(), Some("user@example.com")); - assert_eq!(info.chatgpt_plan_type.as_deref(), Some("pro")); + assert_eq!( + info.chatgpt_plan_type, + Some(PlanType::Known(KnownPlan::Pro)) + ); } } diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 61ab01e965..443c54aa9b 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -537,8 +537,8 @@ impl HistoryCell { lines.push(Line::from(" • Signed in with ChatGPT")); let info = tokens.id_token; - if let Some(email) = info.email { - lines.push(Line::from(vec![" • Login: ".into(), email.into()])); + if let Some(email) = &info.email { + lines.push(Line::from(vec![" • Login: ".into(), email.clone().into()])); } match auth.openai_api_key.as_deref() { @@ -549,9 +549,8 @@ impl HistoryCell { } _ => { let plan_text = info - .chatgpt_plan_type - .as_deref() - .map(title_case) + .get_chatgpt_plan_type() + .map(|s| title_case(&s)) .unwrap_or_else(|| "Unknown".to_string()); lines.push(Line::from(vec![" • Plan: ".into(), plan_text.into()])); } From fa0051190b0f4a28536a0de51bd921479b083562 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Thu, 7 Aug 2025 18:24:34 -0700 Subject: [PATCH 0099/1309] Adjust error messages (#1969) image --- codex-rs/core/src/client.rs | 7 +++- codex-rs/core/src/codex.rs | 4 +- codex-rs/core/src/error.rs | 70 ++++++++++++++++++++++++++++++-- codex-rs/login/src/lib.rs | 27 +++++++----- codex-rs/login/src/token_data.rs | 7 ++++ 5 files changed, 98 insertions(+), 17 deletions(-) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 34aecad17a..0caf1170a6 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -31,6 +31,7 @@ use crate::config_types::ReasoningEffort as ReasoningEffortConfig; use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; use crate::error::CodexErr; use crate::error::Result; +use crate::error::UsageLimitReachedError; use crate::flags::CODEX_RS_SSE_FIXTURE; use crate::model_provider_info::ModelProviderInfo; use crate::model_provider_info::WireApi; @@ -195,7 +196,7 @@ impl ModelClient { if let Some(auth) = auth.as_ref() && auth.mode == AuthMode::ChatGPT - && let Some(account_id) = auth.get_account_id().await + && let Some(account_id) = auth.get_account_id() { req_builder = req_builder.header("chatgpt-account-id", account_id); } @@ -263,7 +264,9 @@ impl ModelClient { }) = body { if r#type == "usage_limit_reached" { - return Err(CodexErr::UsageLimitReached); + return Err(CodexErr::UsageLimitReached(UsageLimitReachedError { + plan_type: auth.and_then(|a| a.get_plan_type()), + })); } else if r#type == "usage_not_included" { return Err(CodexErr::UsageNotIncluded); } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index aaef73ded9..385361e8ff 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1290,7 +1290,9 @@ async fn run_turn( Ok(output) => return Ok(output), Err(CodexErr::Interrupted) => return Err(CodexErr::Interrupted), Err(CodexErr::EnvVar(var)) => return Err(CodexErr::EnvVar(var)), - Err(e @ (CodexErr::UsageLimitReached | CodexErr::UsageNotIncluded)) => return Err(e), + Err(e @ (CodexErr::UsageLimitReached(_) | CodexErr::UsageNotIncluded)) => { + return Err(e); + } Err(e) => { // Use the configured provider-specific stream retry budget. let max_retries = sess.client.get_provider().stream_max_retries(); diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index f6394b71ce..7d6dc2cc8d 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -62,14 +62,16 @@ pub enum CodexErr { #[error("unexpected status {0}: {1}")] UnexpectedStatus(StatusCode, String), - #[error("Usage limit has been reached")] - UsageLimitReached, + #[error("{0}")] + UsageLimitReached(UsageLimitReachedError), - #[error("Usage not included with the plan")] + #[error( + "To use Codex with your ChatGPT plan, upgrade to Plus: https://openai.com/chatgpt/pricing." + )] UsageNotIncluded, #[error( - "We’re currently experiencing high demand, which may cause temporary errors. We’re adding capacity in East and West Europe to restore normal service." + "We're currently experiencing high demand, which may cause temporary errors. We’re adding capacity in East and West Europe to restore normal service." )] InternalServerError, @@ -115,6 +117,30 @@ pub enum CodexErr { EnvVar(EnvVarError), } +#[derive(Debug)] +pub struct UsageLimitReachedError { + pub plan_type: Option, +} + +impl std::fmt::Display for UsageLimitReachedError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(plan_type) = &self.plan_type + && plan_type == "plus" + { + write!( + f, + "You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing), or wait for limits to reset (every 5h and every week.)." + )?; + } else { + write!( + f, + "You've hit usage your usage limit. Limits reset every 5h and every week." + )?; + } + Ok(()) + } +} + #[derive(Debug)] pub struct EnvVarError { /// Name of the environment variable that is missing. @@ -150,3 +176,39 @@ pub fn get_error_message_ui(e: &CodexErr) -> String { _ => e.to_string(), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn usage_limit_reached_error_formats_plus_plan() { + let err = UsageLimitReachedError { + plan_type: Some("plus".to_string()), + }; + assert_eq!( + err.to_string(), + "You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing), or wait for limits to reset (every 5h and every week.)." + ); + } + + #[test] + fn usage_limit_reached_error_formats_default_when_none() { + let err = UsageLimitReachedError { plan_type: None }; + assert_eq!( + err.to_string(), + "You've hit usage your usage limit. Limits reset every 5h and every week." + ); + } + + #[test] + fn usage_limit_reached_error_formats_default_for_other_plans() { + let err = UsageLimitReachedError { + plan_type: Some("pro".to_string()), + }; + assert_eq!( + err.to_string(), + "You've hit usage your usage limit. Limits reset every 5h and every week." + ); + } +} diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index 7e693ccdf8..2a8f6749b4 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -68,8 +68,7 @@ impl CodexAuth { } pub async fn get_token_data(&self) -> Result { - #[expect(clippy::unwrap_used)] - let auth_dot_json = self.auth_dot_json.lock().unwrap().clone(); + let auth_dot_json: Option = self.get_current_auth_json(); match auth_dot_json { Some(AuthDotJson { tokens: Some(mut tokens), @@ -124,15 +123,23 @@ impl CodexAuth { } } - pub async fn get_account_id(&self) -> Option { - match self.mode { - AuthMode::ApiKey => None, - AuthMode::ChatGPT => { - let token_data = self.get_token_data().await.ok()?; + pub fn get_account_id(&self) -> Option { + self.get_current_token_data() + .and_then(|t| t.account_id.clone()) + } - token_data.account_id.clone() - } - } + pub fn get_plan_type(&self) -> Option { + self.get_current_token_data() + .and_then(|t| t.id_token.chatgpt_plan_type.as_ref().map(|p| p.as_string())) + } + + fn get_current_auth_json(&self) -> Option { + #[expect(clippy::unwrap_used)] + self.auth_dot_json.lock().unwrap().clone() + } + + fn get_current_token_data(&self) -> Option { + self.get_current_auth_json().and_then(|t| t.tokens.clone()) } /// Consider this private to integration tests. diff --git a/codex-rs/login/src/token_data.rs b/codex-rs/login/src/token_data.rs index 86ddaf5819..fb4d83950f 100644 --- a/codex-rs/login/src/token_data.rs +++ b/codex-rs/login/src/token_data.rs @@ -67,6 +67,13 @@ impl PlanType { } } } + + pub fn as_string(&self) -> String { + match self { + Self::Known(known) => format!("{known:?}").to_lowercase(), + Self::Unknown(s) => s.clone(), + } + } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] From 2b7139859ec1edcdfe271b1f7615f308f8e60a53 Mon Sep 17 00:00:00 2001 From: easong-openai Date: Thu, 7 Aug 2025 18:26:47 -0700 Subject: [PATCH 0100/1309] Streaming markdown (#1920) We wait until we have an entire newline, then format it with markdown and stream in to the UI. This reduces time to first token but is the right thing to do with our current rendering model IMO. Also lets us add word wrapping! --- codex-rs/core/src/client.rs | 12 +- codex-rs/tui/src/app.rs | 32 +- codex-rs/tui/src/app_event.rs | 4 + .../tui/src/bottom_pane/live_ring_widget.rs | 45 -- codex-rs/tui/src/bottom_pane/mod.rs | 152 +---- codex-rs/tui/src/chatwidget.rs | 568 +++++++++++------- codex-rs/tui/src/chatwidget_stream_tests.rs | 392 ++++++++++++ codex-rs/tui/src/history_cell.rs | 14 +- codex-rs/tui/src/insert_history.rs | 191 ++++-- codex-rs/tui/src/lib.rs | 3 + codex-rs/tui/src/markdown.rs | 322 +++++++++- codex-rs/tui/src/markdown_stream.rs | 565 +++++++++++++++++ codex-rs/tui/tests/vt100_history.rs | 44 +- codex-rs/tui/tests/vt100_streaming_no_dup.rs | 77 +++ 14 files changed, 1940 insertions(+), 481 deletions(-) delete mode 100644 codex-rs/tui/src/bottom_pane/live_ring_widget.rs create mode 100644 codex-rs/tui/src/chatwidget_stream_tests.rs create mode 100644 codex-rs/tui/src/markdown_stream.rs create mode 100644 codex-rs/tui/tests/vt100_streaming_no_dup.rs diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 0caf1170a6..d19f73d6e0 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -504,11 +504,17 @@ async fn process_sse( | "response.in_progress" | "response.output_item.added" | "response.output_text.done" - | "response.reasoning_summary_part.added" - | "response.reasoning_summary_text.done" => { - // Currently, we ignore these events, but we handle them + | "response.reasoning_summary_part.added" => { + // Currently, we ignore this event, but we handle it // separately to skip the logging message in the `other` case. } + "response.reasoning_summary_text.done" => { + // End reasoning summary with a blank separator. + let event = ResponseEvent::ReasoningSummaryDelta("\n\n".to_string()); + if tx_event.send(Ok(event)).await.is_err() { + return; + } + } other => debug!(other, "sse event"), } } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 86d7414151..5d189e91bb 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -64,6 +64,9 @@ pub(crate) struct App<'a> { pending_history_lines: Vec>, enhanced_keys_supported: bool, + + /// Controls the animation thread that sends CommitTick events. + commit_anim_running: Arc, } /// Aggregate parameters needed to create a `ChatWidget`, as creation may be @@ -173,6 +176,7 @@ impl App<'_> { file_search, pending_redraw, enhanced_keys_supported, + commit_anim_running: Arc::new(AtomicBool::new(false)), } } @@ -189,7 +193,7 @@ impl App<'_> { // redraw is already pending so we can return early. if self .pending_redraw - .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) .is_err() { return; @@ -200,7 +204,7 @@ impl App<'_> { thread::spawn(move || { thread::sleep(REDRAW_DEBOUNCE); tx.send(AppEvent::Redraw); - pending_redraw.store(false, Ordering::SeqCst); + pending_redraw.store(false, Ordering::Release); }); } @@ -221,6 +225,30 @@ impl App<'_> { AppEvent::Redraw => { std::io::stdout().sync_update(|_| self.draw_next_frame(terminal))??; } + AppEvent::StartCommitAnimation => { + if self + .commit_anim_running + .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) + .is_ok() + { + let tx = self.app_event_tx.clone(); + let running = self.commit_anim_running.clone(); + thread::spawn(move || { + while running.load(Ordering::Relaxed) { + thread::sleep(Duration::from_millis(50)); + tx.send(AppEvent::CommitTick); + } + }); + } + } + AppEvent::StopCommitAnimation => { + self.commit_anim_running.store(false, Ordering::Release); + } + AppEvent::CommitTick => { + if let AppState::Chat { widget } = &mut self.app_state { + widget.on_commit_tick(); + } + } AppEvent::KeyEvent(key_event) => { match key_event { KeyEvent { diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 7f96fe1e47..9965a91ebc 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -50,6 +50,10 @@ pub(crate) enum AppEvent { InsertHistory(Vec>), + StartCommitAnimation, + StopCommitAnimation, + CommitTick, + /// Onboarding: result of login_with_chatgpt. OnboardingAuthComplete(Result<(), String>), OnboardingComplete(ChatWidgetArgs), diff --git a/codex-rs/tui/src/bottom_pane/live_ring_widget.rs b/codex-rs/tui/src/bottom_pane/live_ring_widget.rs deleted file mode 100644 index 13f91acc5d..0000000000 --- a/codex-rs/tui/src/bottom_pane/live_ring_widget.rs +++ /dev/null @@ -1,45 +0,0 @@ -use ratatui::buffer::Buffer; -use ratatui::layout::Rect; -use ratatui::text::Line; -use ratatui::widgets::Paragraph; -use ratatui::widgets::WidgetRef; - -/// Minimal rendering-only widget for the transient ring rows. -pub(crate) struct LiveRingWidget { - max_rows: u16, - rows: Vec>, // newest at the end -} - -impl LiveRingWidget { - pub fn new() -> Self { - Self { - max_rows: 3, - rows: Vec::new(), - } - } - - pub fn set_max_rows(&mut self, n: u16) { - self.max_rows = n.max(1); - } - - pub fn set_rows(&mut self, rows: Vec>) { - self.rows = rows; - } - - pub fn desired_height(&self, _width: u16) -> u16 { - let len = self.rows.len() as u16; - len.min(self.max_rows) - } -} - -impl WidgetRef for LiveRingWidget { - fn render_ref(&self, area: Rect, buf: &mut Buffer) { - if area.height == 0 { - return; - } - let visible = self.rows.len().saturating_sub(self.max_rows as usize); - let slice = &self.rows[visible..]; - let para = Paragraph::new(slice.to_vec()); - para.render_ref(area, buf); - } -} diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 0c8610470c..7282650841 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -9,7 +9,6 @@ use codex_file_search::FileMatch; use crossterm::event::KeyEvent; use ratatui::buffer::Buffer; use ratatui::layout::Rect; -use ratatui::text::Line; use ratatui::widgets::WidgetRef; mod approval_modal_view; @@ -18,7 +17,6 @@ mod chat_composer; mod chat_composer_history; mod command_popup; mod file_search_popup; -mod live_ring_widget; mod popup_consts; mod scroll_state; mod selection_popup_common; @@ -57,10 +55,6 @@ pub(crate) struct BottomPane<'a> { /// not replace the composer; it augments it. live_status: Option, - /// Optional transient ring shown above the composer. This is a rendering-only - /// container used during development before we wire it to ChatWidget events. - live_ring: Option, - /// True if the active view is the StatusIndicatorView that replaces the /// composer during a running task. status_view_active: bool, @@ -88,7 +82,6 @@ impl BottomPane<'_> { is_task_running: false, ctrl_c_quit_hint: false, live_status: None, - live_ring: None, status_view_active: false, } } @@ -99,26 +92,14 @@ impl BottomPane<'_> { .as_ref() .map(|s| s.desired_height(width)) .unwrap_or(0); - let ring_h = self - .live_ring - .as_ref() - .map(|r| r.desired_height(width)) - .unwrap_or(0); let view_height = if let Some(view) = self.active_view.as_ref() { - // Add a single blank spacer line between live ring and status view when active. - let spacer = if self.live_ring.is_some() && self.status_view_active { - 1 - } else { - 0 - }; - spacer + view.desired_height(width) + view.desired_height(width) } else { self.composer.desired_height(width) }; overlay_status_h - .saturating_add(ring_h) .saturating_add(view_height) .saturating_add(Self::BOTTOM_PAD_LINES) } @@ -352,43 +333,11 @@ impl BottomPane<'_> { self.composer.on_file_search_result(query, matches); self.request_redraw(); } - - /// Set the rows and cap for the transient live ring overlay. - pub(crate) fn set_live_ring_rows(&mut self, max_rows: u16, rows: Vec>) { - let mut w = live_ring_widget::LiveRingWidget::new(); - w.set_max_rows(max_rows); - w.set_rows(rows); - self.live_ring = Some(w); - } - - pub(crate) fn clear_live_ring(&mut self) { - self.live_ring = None; - } - - // Removed restart_live_status_with_text – no longer used by the current streaming UI. } impl WidgetRef for &BottomPane<'_> { fn render_ref(&self, area: Rect, buf: &mut Buffer) { let mut y_offset = 0u16; - if let Some(ring) = &self.live_ring { - let live_h = ring.desired_height(area.width).min(area.height); - if live_h > 0 { - let live_rect = Rect { - x: area.x, - y: area.y, - width: area.width, - height: live_h, - }; - ring.render_ref(live_rect, buf); - y_offset = live_h; - } - } - // Spacer between live ring and status view when active - if self.live_ring.is_some() && self.status_view_active && y_offset < area.height { - // Leave one empty line - y_offset = y_offset.saturating_add(1); - } if let Some(status) = &self.live_status { let live_h = status .desired_height(area.width) @@ -438,7 +387,6 @@ mod tests { use crate::app_event::AppEvent; use ratatui::buffer::Buffer; use ratatui::layout::Rect; - use ratatui::text::Line; use std::path::PathBuf; use std::sync::mpsc::channel; @@ -466,103 +414,7 @@ mod tests { assert_eq!(CancellationEvent::Ignored, pane.on_ctrl_c()); } - #[test] - fn live_ring_renders_above_composer() { - let (tx_raw, _rx) = channel::(); - let tx = AppEventSender::new(tx_raw); - let mut pane = BottomPane::new(BottomPaneParams { - app_event_tx: tx, - has_input_focus: true, - enhanced_keys_supported: false, - }); - - // Provide 4 rows with max_rows=3; only the last 3 should be visible. - pane.set_live_ring_rows( - 3, - vec![ - Line::from("one".to_string()), - Line::from("two".to_string()), - Line::from("three".to_string()), - Line::from("four".to_string()), - ], - ); - - let area = Rect::new(0, 0, 10, 5); - let mut buf = Buffer::empty(area); - (&pane).render_ref(area, &mut buf); - - // Extract the first 3 rows and assert they contain the last three lines. - let mut lines: Vec = Vec::new(); - for y in 0..3 { - let mut s = String::new(); - for x in 0..area.width { - s.push(buf[(x, y)].symbol().chars().next().unwrap_or(' ')); - } - lines.push(s.trim_end().to_string()); - } - assert_eq!(lines, vec!["two", "three", "four"]); - } - - #[test] - fn status_indicator_visible_with_live_ring() { - let (tx_raw, _rx) = channel::(); - let tx = AppEventSender::new(tx_raw); - let mut pane = BottomPane::new(BottomPaneParams { - app_event_tx: tx, - has_input_focus: true, - enhanced_keys_supported: false, - }); - - // Simulate task running which replaces composer with the status indicator. - pane.set_task_running(true); - pane.update_status_text("waiting for model".to_string()); - - // Provide 2 rows in the live ring (e.g., streaming CoT) and ensure the - // status indicator remains visible below them. - pane.set_live_ring_rows( - 2, - vec![ - Line::from("cot1".to_string()), - Line::from("cot2".to_string()), - ], - ); - - // Allow some frames so the dot animation is present. - std::thread::sleep(std::time::Duration::from_millis(120)); - - // Height should include both ring rows, 1 spacer, and the 1-line status. - let area = Rect::new(0, 0, 30, 4); - let mut buf = Buffer::empty(area); - (&pane).render_ref(area, &mut buf); - - // Top two rows are the live ring. - let mut r0 = String::new(); - let mut r1 = String::new(); - for x in 0..area.width { - r0.push(buf[(x, 0)].symbol().chars().next().unwrap_or(' ')); - r1.push(buf[(x, 1)].symbol().chars().next().unwrap_or(' ')); - } - assert!(r0.contains("cot1"), "expected first live row: {r0:?}"); - assert!(r1.contains("cot2"), "expected second live row: {r1:?}"); - - // Row 2 is the spacer (blank) - let mut r2 = String::new(); - for x in 0..area.width { - r2.push(buf[(x, 2)].symbol().chars().next().unwrap_or(' ')); - } - assert!(r2.trim().is_empty(), "expected blank spacer line: {r2:?}"); - - // Bottom row is the status line; it should contain the left bar and "Working". - let mut r3 = String::new(); - for x in 0..area.width { - r3.push(buf[(x, 3)].symbol().chars().next().unwrap_or(' ')); - } - assert_eq!(buf[(0, 3)].symbol().chars().next().unwrap_or(' '), '▌'); - assert!( - r3.contains("Working"), - "expected Working header in status line: {r3:?}" - ); - } + // live ring removed; related tests deleted. #[test] fn overlay_not_shown_above_approval_modal() { diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 8a47353cbf..075154016f 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::collections::VecDeque; use std::path::PathBuf; use std::sync::Arc; @@ -45,13 +46,14 @@ use crate::bottom_pane::BottomPane; use crate::bottom_pane::BottomPaneParams; use crate::bottom_pane::CancellationEvent; use crate::bottom_pane::InputResult; +use crate::exec_command::strip_bash_lc_and_escape; use crate::history_cell::CommandOutput; use crate::history_cell::HistoryCell; use crate::history_cell::PatchEventType; -use crate::live_wrap::RowBuilder; +use crate::markdown_stream::MarkdownNewlineCollector; +use crate::markdown_stream::RenderedLineStreamer; use crate::user_approval_widget::ApprovalRequest; use codex_file_search::FileMatch; -use ratatui::style::Stylize; struct RunningCommand { command: Vec, @@ -68,17 +70,21 @@ pub(crate) struct ChatWidget<'a> { initial_user_message: Option, total_token_usage: TokenUsage, last_token_usage: TokenUsage, - reasoning_buffer: String, - content_buffer: String, - // Buffer for streaming assistant answer text; we do not surface partial - // We wait for the final AgentMessage event and then emit the full text - // at once into scrollback so the history contains a single message. - answer_buffer: String, + // Newline-gated markdown streaming state + reasoning_collector: MarkdownNewlineCollector, + answer_collector: MarkdownNewlineCollector, + reasoning_streamer: RenderedLineStreamer, + answer_streamer: RenderedLineStreamer, running_commands: HashMap, - live_builder: RowBuilder, current_stream: Option, - stream_header_emitted: bool, + // Track header emission per stream kind to avoid cross-stream duplication + answer_header_emitted: bool, + reasoning_header_emitted: bool, live_max_rows: u16, + task_complete_pending: bool, + finishing_after_drain: bool, + // Queue of interruptive UI events deferred during an active write cycle + interrupt_queue: VecDeque, } struct UserMessage { @@ -92,6 +98,15 @@ enum StreamKind { Reasoning, } +#[derive(Debug)] +enum QueuedInterrupt { + ExecApproval(String, ExecApprovalRequestEvent), + ApplyPatchApproval(String, ApplyPatchApprovalRequestEvent), + ExecBegin(ExecCommandBeginEvent), + McpBegin(McpToolCallBeginEvent), + McpEnd(McpToolCallEndEvent), +} + impl From for UserMessage { fn from(text: String) -> Self { Self { @@ -110,19 +125,173 @@ fn create_initial_user_message(text: String, image_paths: Vec) -> Optio } impl ChatWidget<'_> { + fn header_line(kind: StreamKind) -> ratatui::text::Line<'static> { + use ratatui::style::Stylize; + match kind { + StreamKind::Reasoning => ratatui::text::Line::from("thinking".magenta().italic()), + StreamKind::Answer => ratatui::text::Line::from("codex".magenta().bold()), + } + } + fn line_is_blank(line: &ratatui::text::Line<'_>) -> bool { + if line.spans.is_empty() { + return true; + } + line.spans.iter().all(|s| s.content.trim().is_empty()) + } + /// Periodic tick to commit at most one queued line to history with a small delay, + /// animating the output. + pub(crate) fn on_commit_tick(&mut self) { + // Choose the active streamer + let (streamer, kind_opt) = match self.current_stream { + Some(StreamKind::Reasoning) => { + (&mut self.reasoning_streamer, Some(StreamKind::Reasoning)) + } + Some(StreamKind::Answer) => (&mut self.answer_streamer, Some(StreamKind::Answer)), + None => { + // No active stream. Nothing to animate. + return; + } + }; + + // Prepare header if needed + let mut lines: Vec> = Vec::new(); + if let Some(k) = kind_opt { + let header_needed = match k { + StreamKind::Reasoning => !self.reasoning_header_emitted, + StreamKind::Answer => !self.answer_header_emitted, + }; + if header_needed { + lines.push(Self::header_line(k)); + match k { + StreamKind::Reasoning => self.reasoning_header_emitted = true, + StreamKind::Answer => self.answer_header_emitted = true, + } + } + } + + let step = streamer.step(self.live_max_rows as usize); + if !step.history.is_empty() || !lines.is_empty() { + lines.extend(step.history); + self.app_event_tx.send(AppEvent::InsertHistory(lines)); + } + + // If streamer is now idle and there is no more active stream data, finalize state. + let is_idle = streamer.is_idle(); + if is_idle { + // Stop animation ticks between bursts. + self.app_event_tx.send(AppEvent::StopCommitAnimation); + if self.finishing_after_drain { + // Final cleanup once fully drained at end-of-stream. + self.current_stream = None; + self.finishing_after_drain = false; + if self.task_complete_pending { + self.bottom_pane.set_task_running(false); + self.task_complete_pending = false; + } + // After the write cycle completes, release any queued interrupts. + self.flush_interrupt_queue(); + } + } + } + fn is_write_cycle_active(&self) -> bool { + self.current_stream.is_some() + } + + fn flush_interrupt_queue(&mut self) { + while let Some(q) = self.interrupt_queue.pop_front() { + match q { + QueuedInterrupt::ExecApproval(id, ev) => self.handle_exec_approval_now(id, ev), + QueuedInterrupt::ApplyPatchApproval(id, ev) => { + self.handle_apply_patch_approval_now(id, ev) + } + QueuedInterrupt::ExecBegin(ev) => self.handle_exec_begin_now(ev), + QueuedInterrupt::McpBegin(ev) => self.handle_mcp_begin_now(ev), + QueuedInterrupt::McpEnd(ev) => self.handle_mcp_end_now(ev), + } + } + } + + fn handle_exec_approval_now(&mut self, id: String, ev: ExecApprovalRequestEvent) { + // Log a background summary immediately so the history is chronological. + let cmdline = strip_bash_lc_and_escape(&ev.command); + let text = format!( + "command requires approval:\n$ {cmdline}{reason}", + reason = ev + .reason + .as_ref() + .map(|r| format!("\n{r}")) + .unwrap_or_default() + ); + self.add_to_history(HistoryCell::new_background_event(text)); + + let request = ApprovalRequest::Exec { + id, + command: ev.command, + cwd: ev.cwd, + reason: ev.reason, + }; + self.bottom_pane.push_approval_request(request); + self.request_redraw(); + } + + fn handle_apply_patch_approval_now(&mut self, id: String, ev: ApplyPatchApprovalRequestEvent) { + self.add_to_history(HistoryCell::new_patch_event( + PatchEventType::ApprovalRequest, + ev.changes.clone(), + )); + + let request = ApprovalRequest::ApplyPatch { + id, + reason: ev.reason, + grant_root: ev.grant_root, + }; + self.bottom_pane.push_approval_request(request); + self.request_redraw(); + } + + fn handle_exec_begin_now(&mut self, ev: ExecCommandBeginEvent) { + // Ensure the status indicator is visible while the command runs. + self.bottom_pane + .update_status_text("running command".to_string()); + self.running_commands.insert( + ev.call_id.clone(), + RunningCommand { + command: ev.command.clone(), + cwd: ev.cwd.clone(), + }, + ); + self.active_history_cell = Some(HistoryCell::new_active_exec_command(ev.command)); + } + + fn handle_mcp_begin_now(&mut self, ev: McpToolCallBeginEvent) { + self.add_to_history(HistoryCell::new_active_mcp_tool_call(ev.invocation)); + } + + fn handle_mcp_end_now(&mut self, ev: McpToolCallEndEvent) { + self.add_to_history(HistoryCell::new_completed_mcp_tool_call( + 80, + ev.invocation, + ev.duration, + ev.result + .as_ref() + .map(|r| r.is_error.unwrap_or(false)) + .unwrap_or(false), + ev.result, + )); + } fn interrupt_running_task(&mut self) { if self.bottom_pane.is_task_running() { self.active_history_cell = None; self.bottom_pane.clear_ctrl_c_quit_hint(); self.submit_op(Op::Interrupt); self.bottom_pane.set_task_running(false); - self.bottom_pane.clear_live_ring(); - self.live_builder = RowBuilder::new(self.live_builder.width()); + self.reasoning_collector.clear(); + self.answer_collector.clear(); + self.reasoning_streamer.clear(); + self.answer_streamer.clear(); self.current_stream = None; - self.stream_header_emitted = false; - self.answer_buffer.clear(); - self.reasoning_buffer.clear(); - self.content_buffer.clear(); + self.answer_header_emitted = false; + self.reasoning_header_emitted = false; self.request_redraw(); } } @@ -137,24 +306,7 @@ impl ChatWidget<'_> { ]) .areas(area) } - fn emit_stream_header(&mut self, kind: StreamKind) { - use ratatui::text::Line as RLine; - if self.stream_header_emitted { - return; - } - let header = match kind { - StreamKind::Reasoning => RLine::from("thinking".magenta().italic()), - StreamKind::Answer => RLine::from("codex".magenta().bold()), - }; - self.app_event_tx - .send(AppEvent::InsertHistory(vec![header])); - self.stream_header_emitted = true; - } - fn finalize_active_stream(&mut self) { - if let Some(kind) = self.current_stream { - self.finalize_stream(kind); - } - } + pub(crate) fn new( config: Config, app_event_tx: AppEventSender, @@ -216,14 +368,18 @@ impl ChatWidget<'_> { ), total_token_usage: TokenUsage::default(), last_token_usage: TokenUsage::default(), - reasoning_buffer: String::new(), - content_buffer: String::new(), - answer_buffer: String::new(), + reasoning_collector: MarkdownNewlineCollector::new(), + answer_collector: MarkdownNewlineCollector::new(), + reasoning_streamer: RenderedLineStreamer::new(), + answer_streamer: RenderedLineStreamer::new(), running_commands: HashMap::new(), - live_builder: RowBuilder::new(80), current_stream: None, - stream_header_emitted: false, + answer_header_emitted: false, + reasoning_header_emitted: false, live_max_rows: 3, + task_complete_pending: false, + finishing_after_drain: false, + interrupt_queue: VecDeque::new(), } } @@ -320,7 +476,6 @@ impl ChatWidget<'_> { } EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta }) => { self.begin_stream(StreamKind::Answer); - self.answer_buffer.push_str(&delta); self.stream_push_and_maybe_commit(&delta); self.request_redraw(); } @@ -328,7 +483,6 @@ impl ChatWidget<'_> { // Stream CoT into the live pane; keep input visible and commit // overflow rows incrementally to scrollback. self.begin_stream(StreamKind::Reasoning); - self.reasoning_buffer.push_str(&delta); self.stream_push_and_maybe_commit(&delta); self.request_redraw(); } @@ -342,7 +496,6 @@ impl ChatWidget<'_> { }) => { // Treat raw reasoning content the same as summarized reasoning for UI flow. self.begin_stream(StreamKind::Reasoning); - self.reasoning_buffer.push_str(&delta); self.stream_push_and_maybe_commit(&delta); self.request_redraw(); } @@ -362,9 +515,18 @@ impl ChatWidget<'_> { EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message: _, }) => { - self.bottom_pane.set_task_running(false); - self.bottom_pane.clear_live_ring(); - self.request_redraw(); + // Defer clearing status/live ring until streaming fully completes. + let streaming_active = match self.current_stream { + Some(StreamKind::Reasoning) => !self.reasoning_streamer.is_idle(), + Some(StreamKind::Answer) => !self.answer_streamer.is_idle(), + None => false, + }; + if streaming_active { + self.task_complete_pending = true; + } else { + self.bottom_pane.set_task_running(false); + self.request_redraw(); + } } EventMsg::TokenCount(token_usage) => { self.total_token_usage = add_token_usage(&self.total_token_usage, &token_usage); @@ -378,83 +540,42 @@ impl ChatWidget<'_> { EventMsg::Error(ErrorEvent { message }) => { self.add_to_history(HistoryCell::new_error_event(message.clone())); self.bottom_pane.set_task_running(false); - self.bottom_pane.clear_live_ring(); - self.live_builder = RowBuilder::new(self.live_builder.width()); + self.reasoning_collector.clear(); + self.answer_collector.clear(); + self.reasoning_streamer.clear(); + self.answer_streamer.clear(); self.current_stream = None; - self.stream_header_emitted = false; - self.answer_buffer.clear(); - self.reasoning_buffer.clear(); - self.content_buffer.clear(); + self.answer_header_emitted = false; + self.reasoning_header_emitted = false; self.request_redraw(); } EventMsg::PlanUpdate(update) => { // Commit plan updates directly to history (no status-line preview). self.add_to_history(HistoryCell::new_plan_update(update)); } - EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { - call_id: _, - command, - cwd, - reason, - }) => { - self.finalize_active_stream(); - let request = ApprovalRequest::Exec { - id, - command, - cwd, - reason, - }; - self.bottom_pane.push_approval_request(request); - self.request_redraw(); + EventMsg::ExecApprovalRequest(ev) => { + if self.is_write_cycle_active() { + self.interrupt_queue + .push_back(QueuedInterrupt::ExecApproval(id, ev)); + } else { + self.handle_exec_approval_now(id, ev); + } } - EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { - call_id: _, - changes, - reason, - grant_root, - }) => { - self.finalize_active_stream(); - // ------------------------------------------------------------------ - // Before we even prompt the user for approval we surface the patch - // summary in the main conversation so that the dialog appears in a - // sensible chronological order: - // (1) codex → proposes patch (HistoryCell::PendingPatch) - // (2) UI → asks for approval (BottomPane) - // This mirrors how command execution is shown (command begins → - // approval dialog) and avoids surprising the user with a modal - // prompt before they have seen *what* is being requested. - // ------------------------------------------------------------------ - self.add_to_history(HistoryCell::new_patch_event( - PatchEventType::ApprovalRequest, - changes, - )); - - // Now surface the approval request in the BottomPane as before. - let request = ApprovalRequest::ApplyPatch { - id, - reason, - grant_root, - }; - self.bottom_pane.push_approval_request(request); - self.request_redraw(); + EventMsg::ApplyPatchApprovalRequest(ev) => { + if self.is_write_cycle_active() { + self.interrupt_queue + .push_back(QueuedInterrupt::ApplyPatchApproval(id, ev)); + } else { + self.handle_apply_patch_approval_now(id, ev); + } } - EventMsg::ExecCommandBegin(ExecCommandBeginEvent { - call_id, - command, - cwd, - }) => { - self.finalize_active_stream(); - // Ensure the status indicator is visible while the command runs. - self.bottom_pane - .update_status_text("running command".to_string()); - self.running_commands.insert( - call_id, - RunningCommand { - command: command.clone(), - cwd: cwd.clone(), - }, - ); - self.active_history_cell = Some(HistoryCell::new_active_exec_command(command)); + EventMsg::ExecCommandBegin(ev) => { + if self.is_write_cycle_active() { + self.interrupt_queue + .push_back(QueuedInterrupt::ExecBegin(ev)); + } else { + self.handle_exec_begin_now(ev); + } } EventMsg::ExecCommandOutputDelta(_) => { // TODO @@ -493,29 +614,20 @@ impl ChatWidget<'_> { }, )); } - EventMsg::McpToolCallBegin(McpToolCallBeginEvent { - call_id: _, - invocation, - }) => { - self.finalize_active_stream(); - self.add_to_history(HistoryCell::new_active_mcp_tool_call(invocation)); + EventMsg::McpToolCallBegin(ev) => { + if self.is_write_cycle_active() { + self.interrupt_queue + .push_back(QueuedInterrupt::McpBegin(ev)); + } else { + self.handle_mcp_begin_now(ev); + } } - EventMsg::McpToolCallEnd(McpToolCallEndEvent { - call_id: _, - duration, - invocation, - result, - }) => { - self.add_to_history(HistoryCell::new_completed_mcp_tool_call( - 80, - invocation, - duration, - result - .as_ref() - .map(|r| r.is_error.unwrap_or(false)) - .unwrap_or(false), - result, - )); + EventMsg::McpToolCallEnd(ev) => { + if self.is_write_cycle_active() { + self.interrupt_queue.push_back(QueuedInterrupt::McpEnd(ev)); + } else { + self.handle_mcp_end_now(ev); + } } EventMsg::GetHistoryEntryResponse(event) => { let codex_core::protocol::GetHistoryEntryResponseEvent { @@ -635,62 +747,98 @@ impl ChatWidget<'_> { } } +#[cfg(test)] +impl ChatWidget<'_> { + /// Test-only control to tune the maximum rows shown in the live overlay. + /// Useful for verifying queue-head behavior without changing production defaults. + pub fn test_set_live_max_rows(&mut self, n: u16) { + self.live_max_rows = n; + } +} + impl ChatWidget<'_> { fn begin_stream(&mut self, kind: StreamKind) { if let Some(current) = self.current_stream { if current != kind { - self.finalize_stream(current); + // Synchronously flush the previous stream to keep ordering sane. + let (collector, streamer) = match current { + StreamKind::Reasoning => { + (&mut self.reasoning_collector, &mut self.reasoning_streamer) + } + StreamKind::Answer => (&mut self.answer_collector, &mut self.answer_streamer), + }; + let remaining = collector.finalize_and_drain(&self.config); + if !remaining.is_empty() { + streamer.enqueue(remaining); + } + let step = streamer.drain_all(self.live_max_rows as usize); + let prev_header_emitted = match current { + StreamKind::Reasoning => self.reasoning_header_emitted, + StreamKind::Answer => self.answer_header_emitted, + }; + if !step.history.is_empty() || !prev_header_emitted { + let mut lines: Vec> = Vec::new(); + if !prev_header_emitted { + lines.push(Self::header_line(current)); + match current { + StreamKind::Reasoning => self.reasoning_header_emitted = true, + StreamKind::Answer => self.answer_header_emitted = true, + } + } + lines.extend(step.history); + // Ensure at most one blank separator after the flushed block. + if let Some(last) = lines.last() { + if !Self::line_is_blank(last) { + lines.push(ratatui::text::Line::from("")); + } + } else { + lines.push(ratatui::text::Line::from("")); + } + self.app_event_tx.send(AppEvent::InsertHistory(lines)); + } + // Reset for new stream + self.current_stream = None; } } if self.current_stream != Some(kind) { + // Only reset the header flag when switching FROM a different stream kind. + // If current_stream is None (e.g., transient idle), preserve header flags + // to avoid duplicate headers on re-entry into the same stream. + let prev = self.current_stream; self.current_stream = Some(kind); - self.stream_header_emitted = false; - // Clear any previous live content; we're starting a new stream. - self.live_builder = RowBuilder::new(self.live_builder.width()); + if prev.is_some() { + match kind { + StreamKind::Reasoning => self.reasoning_header_emitted = false, + StreamKind::Answer => self.answer_header_emitted = false, + } + } // Ensure the waiting status is visible (composer replaced). self.bottom_pane .update_status_text("waiting for model".to_string()); - self.emit_stream_header(kind); + // No live ring overlay; headers will be inserted with the first commit. } } fn stream_push_and_maybe_commit(&mut self, delta: &str) { - self.live_builder.push_fragment(delta); + // Newline-gated: only consider committing when a newline is present. + let (collector, streamer) = match self.current_stream { + Some(StreamKind::Reasoning) => { + (&mut self.reasoning_collector, &mut self.reasoning_streamer) + } + Some(StreamKind::Answer) => (&mut self.answer_collector, &mut self.answer_streamer), + None => return, + }; - // Commit overflow rows (small batches) while keeping the last N rows visible. - let drained = self - .live_builder - .drain_commit_ready(self.live_max_rows as usize); - if !drained.is_empty() { - let mut lines: Vec> = Vec::new(); - if !self.stream_header_emitted { - match self.current_stream { - Some(StreamKind::Reasoning) => { - lines.push(ratatui::text::Line::from("thinking".magenta().italic())); - } - Some(StreamKind::Answer) => { - lines.push(ratatui::text::Line::from("codex".magenta().bold())); - } - None => {} - } - self.stream_header_emitted = true; + collector.push_delta(delta); + if delta.contains('\n') { + let newly_completed = collector.commit_complete_lines(&self.config); + if !newly_completed.is_empty() { + streamer.enqueue(newly_completed); + // Start or continue commit animation. + self.app_event_tx.send(AppEvent::StartCommitAnimation); } - for r in drained { - lines.push(ratatui::text::Line::from(r.text)); - } - self.app_event_tx.send(AppEvent::InsertHistory(lines)); } - - // Update the live ring overlay lines (text-only, newest at bottom). - let rows = self - .live_builder - .display_rows() - .into_iter() - .map(|r| ratatui::text::Line::from(r.text)) - .collect::>(); - self.bottom_pane - .set_live_ring_rows(self.live_max_rows, rows); } fn finalize_stream(&mut self, kind: StreamKind) { @@ -698,38 +846,21 @@ impl ChatWidget<'_> { // Nothing to do; either already finalized or not the active stream. return; } - // Flush any partial line as a full row, then drain all remaining rows. - self.live_builder.end_line(); - let remaining = self.live_builder.drain_rows(); - // TODO: Re-add markdown rendering for assistant answers and reasoning. - // When finalizing, pass the accumulated text through `markdown::append_markdown` - // to build styled `Line<'static>` entries instead of raw plain text lines. - if !remaining.is_empty() || !self.stream_header_emitted { - let mut lines: Vec> = Vec::new(); - if !self.stream_header_emitted { - match kind { - StreamKind::Reasoning => { - lines.push(ratatui::text::Line::from("thinking".magenta().italic())); - } - StreamKind::Answer => { - lines.push(ratatui::text::Line::from("codex".magenta().bold())); - } - } - self.stream_header_emitted = true; - } - for r in remaining { - lines.push(ratatui::text::Line::from(r.text)); - } - // Close the block with a blank line for readability. - lines.push(ratatui::text::Line::from("")); - self.app_event_tx.send(AppEvent::InsertHistory(lines)); - } + let (collector, streamer) = match kind { + StreamKind::Reasoning => (&mut self.reasoning_collector, &mut self.reasoning_streamer), + StreamKind::Answer => (&mut self.answer_collector, &mut self.answer_streamer), + }; - // Clear the live overlay and reset state for the next stream. - self.live_builder = RowBuilder::new(self.live_builder.width()); - self.bottom_pane.clear_live_ring(); - self.current_stream = None; - self.stream_header_emitted = false; + let remaining = collector.finalize_and_drain(&self.config); + if !remaining.is_empty() { + streamer.enqueue(remaining); + } + // Trailing blank spacer + streamer.enqueue(vec![ratatui::text::Line::from("")]); + // Mark that we should clear state after draining. + self.finishing_after_drain = true; + // Start animation to drain remaining lines. Final cleanup will occur when drained. + self.app_event_tx.send(AppEvent::StartCommitAnimation); } } @@ -770,3 +901,34 @@ fn add_token_usage(current_usage: &TokenUsage, new_usage: &TokenUsage) -> TokenU total_tokens: current_usage.total_tokens + new_usage.total_tokens, } } + +#[cfg(test)] +mod chatwidget_helper_tests { + use super::*; + use crate::app_event::AppEvent; + use crate::app_event_sender::AppEventSender; + use codex_core::config::ConfigOverrides; + use std::sync::mpsc::channel; + + fn test_config() -> Config { + let overrides = ConfigOverrides { + cwd: std::env::current_dir().ok(), + ..Default::default() + }; + match Config::load_with_cli_overrides(vec![], overrides) { + Ok(c) => c, + Err(e) => panic!("load test config: {e}"), + } + } + + #[tokio::test(flavor = "current_thread")] + async fn helpers_are_available_and_do_not_panic() { + let (tx_raw, _rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let cfg = test_config(); + let mut w = ChatWidget::new(cfg, tx, None, Vec::new(), false); + + // Adjust the live ring capacity (no-op for rendering) and ensure no panic. + w.test_set_live_max_rows(4); + } +} diff --git a/codex-rs/tui/src/chatwidget_stream_tests.rs b/codex-rs/tui/src/chatwidget_stream_tests.rs new file mode 100644 index 0000000000..6757209017 --- /dev/null +++ b/codex-rs/tui/src/chatwidget_stream_tests.rs @@ -0,0 +1,392 @@ +#[cfg(test)] +mod tests { + use std::sync::mpsc::{channel, Receiver}; + use std::time::Duration; + + use codex_core::config::Config; + use codex_core::config::ConfigOverrides; +use codex_core::protocol::{ + AgentMessageDeltaEvent, AgentMessageEvent, AgentReasoningDeltaEvent, AgentReasoningEvent, Event, EventMsg, +}; + + use crate::app_event::AppEvent; + use crate::app_event_sender::AppEventSender; + use crate::chatwidget::ChatWidget; + + fn test_config() -> Config { + let overrides = ConfigOverrides { + cwd: std::env::current_dir().ok(), + ..Default::default() + }; + match Config::load_with_cli_overrides(vec![], overrides) { + Ok(c) => c, + Err(e) => panic!("load test config: {e}"), + } + } + + fn recv_insert_history( + rx: &Receiver, + timeout_ms: u64, + ) -> Option>> { + let to = Duration::from_millis(timeout_ms); + match rx.recv_timeout(to) { + Ok(AppEvent::InsertHistory(lines)) => Some(lines), + Ok(_) => None, + Err(_) => None, + } + } + + #[test] + fn widget_streams_on_newline_and_header_once() { + let (tx_raw, rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let config = test_config(); + + let mut w = ChatWidget::new(config.clone(), tx.clone(), None, Vec::new(), false); + + // Start reasoning stream with partial content (no newline): expect no history yet. + w.handle_codex_event(Event { + id: "1".into(), + msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { + delta: "Hello".into(), + }), + }); + + // No history commit before newline. + assert!( + recv_insert_history(&rx, 50).is_none(), + "unexpected history before newline" + ); + + // No live overlay anymore; nothing visible until commit. + + // Push a newline which should cause commit of the first logical line. + w.handle_codex_event(Event { + id: "1".into(), + msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { + delta: " world\nNext".into(), + }), + }); + + let lines = match recv_insert_history(&rx, 200) { + Some(v) => v, + None => panic!("expected history after newline"), + }; + let rendered: Vec = lines + .iter() + .map(|l| { + l.spans + .iter() + .map(|s| s.content.clone()) + .collect::>() + .join("") + }) + .collect(); + + // First commit should include the header and the completed first line once. + assert!( + rendered.iter().any(|s| s.contains("thinking")), + "missing reasoning header: {rendered:?}" + ); + assert!( + rendered.iter().any(|s| s.contains("Hello world")), + "missing committed line: {rendered:?}" + ); + + // Send finalize; expect remaining content to flush and a trailing blank line. + w.handle_codex_event(Event { + id: "1".into(), + msg: EventMsg::AgentReasoning(AgentReasoningEvent { + text: String::new(), + }), + }); + + let lines2 = match recv_insert_history(&rx, 200) { + Some(v) => v, + None => panic!("expected history after finalize"), + }; + let rendered2: Vec = lines2 + .iter() + .map(|l| { + l.spans + .iter() + .map(|s| s.content.clone()) + .collect::>() + .join("") + }) + .collect(); + // Ensure header not repeated on finalize and a blank spacer exists at the end. + let header_count = rendered + .iter() + .chain(rendered2.iter()) + .filter(|s| s.contains("thinking")) + .count(); + assert_eq!(header_count, 1, "reasoning header should be emitted exactly once"); + assert!( + rendered2.last().is_some_and(|s| s.is_empty()), + "expected trailing blank line on finalize" + ); + } +} + +#[cfg(test)] +mod widget_stream_extra { + use super::*; + + #[test] + fn widget_fenced_code_slow_streaming_no_dup() { + let (tx_raw, rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let config = test_config(); + let mut w = ChatWidget::new(config.clone(), tx.clone(), None, Vec::new(), false); + + // Begin answer stream: push opening fence in pieces with no newline -> no history. + for d in ["```", ""] { + w.handle_codex_event(Event { + id: "a".into(), + msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: d.into() }), + }); + assert!(super::recv_insert_history(&rx, 30).is_none(), "no history before newline for fence"); + } + // Newline after fence line. + w.handle_codex_event(Event { + id: "a".into(), + msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: "\n".into() }), + }); + // This may or may not produce a visible line depending on renderer; accept either. + let _ = super::recv_insert_history(&rx, 100); + + // Stream the code line without newline -> no history. + w.handle_codex_event(Event { + id: "a".into(), + msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: "code line".into() }), + }); + assert!(super::recv_insert_history(&rx, 30).is_none(), "no history before newline for code line"); + + // Now newline to commit the code line. + w.handle_codex_event(Event { + id: "a".into(), + msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: "\n".into() }), + }); + let commit1 = match super::recv_insert_history(&rx, 200) { + Some(v) => v, + None => panic!("history after code line newline"), + }; + + // Close fence slowly then newline. + w.handle_codex_event(Event { + id: "a".into(), + msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: "```".into() }), + }); + assert!(super::recv_insert_history(&rx, 30).is_none(), "no history before closing fence newline"); + w.handle_codex_event(Event { + id: "a".into(), + msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: "\n".into() }), + }); + let _ = super::recv_insert_history(&rx, 100); + + // Finalize should not duplicate the code line and should add a trailing blank. + w.handle_codex_event(Event { + id: "a".into(), + msg: EventMsg::AgentMessage(AgentMessageEvent { message: String::new() }), + }); + let commit2 = match super::recv_insert_history(&rx, 200) { + Some(v) => v, + None => panic!("history after finalize"), + }; + + let texts1: Vec = commit1 + .iter() + .map(|l| l.spans.iter().map(|s| s.content.clone()).collect::()) + .collect(); + let texts2: Vec = commit2 + .iter() + .map(|l| l.spans.iter().map(|s| s.content.clone()).collect::()) + .collect(); + let all = [texts1, texts2].concat(); + let code_count = all.iter().filter(|s| s.contains("code line")).count(); + assert_eq!(code_count, 1, "code line should appear exactly once in history: {all:?}"); + assert!(all.iter().all(|s| !s.contains("```")), "backticks should not be shown in history: {all:?}"); + } + + #[test] + fn widget_rendered_trickle_live_ring_head() { + let (tx_raw, rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let config = test_config(); + let mut w = ChatWidget::new(config.clone(), tx.clone(), None, Vec::new(), false); + + // Increase live ring capacity so it can include queue head. + w.test_set_live_max_rows(4); + + // Enqueue 5 completed lines in a single delta. + let payload = "l1\nl2\nl3\nl4\nl5\n".to_string(); + w.handle_codex_event(Event { + id: "b".into(), + msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: payload }), + }); + + // First batch commit: expect header + 3 lines. + let lines = match super::recv_insert_history(&rx, 200) { + Some(v) => v, + None => panic!("history after batch"), + }; + let rendered: Vec = lines + .iter() + .map(|l| l.spans.iter().map(|s| s.content.clone()).collect::()) + .collect(); + assert!(rendered.iter().any(|s| s.contains("codex")), "answer header missing"); + let committed: Vec<_> = rendered.into_iter().filter(|s| s.starts_with('l')).collect(); + assert_eq!(committed.len(), 3, "expected 3 committed lines in first batch"); + + // No live overlay anymore; only committed lines appear in history. + + // Finalize: drain the remaining lines. + w.handle_codex_event(Event { + id: "b".into(), + msg: EventMsg::AgentMessage(AgentMessageEvent { message: String::new() }), + }); + let lines2 = match super::recv_insert_history(&rx, 200) { + Some(v) => v, + None => panic!("history after finalize"), + }; + let rendered2: Vec = lines2 + .iter() + .map(|l| l.spans.iter().map(|s| s.content.clone()).collect::()) + .collect(); + assert!(rendered2.iter().any(|s| s == "l4")); + assert!(rendered2.iter().any(|s| s == "l5")); + assert!(rendered2.last().is_some_and(|s| s.is_empty()), "expected trailing blank line after finalize"); + } + + #[test] + fn widget_reasoning_then_answer_ordering() { + let (tx_raw, rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let config = test_config(); + let mut w = ChatWidget::new(config.clone(), tx.clone(), None, Vec::new(), false); + + // Reasoning: one completed line then finalize. + w.handle_codex_event(Event { + id: "ra".into(), + msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta: "think1\n".into() }), + }); + let r_commit = match super::recv_insert_history(&rx, 200) { + Some(v) => v, + None => panic!("reasoning history"), + }; + w.handle_codex_event(Event { + id: "ra".into(), + msg: EventMsg::AgentReasoning(AgentReasoningEvent { text: String::new() }), + }); + let r_final = match super::recv_insert_history(&rx, 200) { + Some(v) => v, + None => panic!("reasoning finalize"), + }; + + // Answer: one completed line then finalize. + w.handle_codex_event(Event { + id: "ra".into(), + msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: "ans1\n".into() }), + }); + let a_commit = match super::recv_insert_history(&rx, 200) { + Some(v) => v, + None => panic!("answer history"), + }; + w.handle_codex_event(Event { + id: "ra".into(), + msg: EventMsg::AgentMessage(AgentMessageEvent { message: String::new() }), + }); + let a_final = match super::recv_insert_history(&rx, 200) { + Some(v) => v, + None => panic!("answer finalize"), + }; + + let to_texts = |lines: &Vec>| -> Vec { + lines + .iter() + .map(|l| l.spans.iter().map(|s| s.content.clone()).collect::()) + .collect() + }; + let r_all = [to_texts(&r_commit), to_texts(&r_final)].concat(); + let a_all = [to_texts(&a_commit), to_texts(&a_final)].concat(); + + // Expect headers present and in order: reasoning first, then answer. + let r_header_idx = match r_all.iter().position(|s| s.contains("thinking")) { + Some(i) => i, + None => panic!("missing reasoning header"), + }; + let a_header_idx = match a_all.iter().position(|s| s.contains("codex")) { + Some(i) => i, + None => panic!("missing answer header"), + }; + assert!(r_all.iter().any(|s| s == "think1"), "missing reasoning content: {:?}", r_all); + assert!(a_all.iter().any(|s| s == "ans1"), "missing answer content: {:?}", a_all); + // Implicitly, reasoning events happened before answer events if we got here without timeouts. + assert_eq!(r_header_idx, 0, "reasoning header should be first in its batch"); + assert_eq!(a_header_idx, 0, "answer header should be first in its batch"); + } + + #[test] + fn header_not_repeated_across_pauses() { + let (tx_raw, rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let config = test_config(); + let mut w = ChatWidget::new(config.clone(), tx.clone(), None, Vec::new(), false); + + // Begin reasoning, enqueue first line, start animation. + w.handle_codex_event(Event { + id: "r1".into(), + msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta: "first\n".into() }), + }); + // Simulate one animation tick: should emit header + first. + w.on_commit_tick(); + let lines1 = super::recv_insert_history(&rx, 200).expect("history after first tick"); + let texts1: Vec = lines1 + .iter() + .map(|l| l.spans.iter().map(|s| s.content.clone()).collect::()) + .collect(); + assert!(texts1.iter().any(|s| s.contains("thinking")), "missing header on first tick: {texts1:?}"); + assert!(texts1.iter().any(|s| s == "first"), "missing first line: {texts1:?}"); + + // Stop ticks naturally by draining queue (second tick consumes nothing). + w.on_commit_tick(); + let _ = super::recv_insert_history(&rx, 100); + + // Later, enqueue another completed line; header must NOT repeat. + w.handle_codex_event(Event { + id: "r1".into(), + msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta: "second\n".into() }), + }); + w.on_commit_tick(); + let lines2 = super::recv_insert_history(&rx, 200).expect("history after second tick"); + let texts2: Vec = lines2 + .iter() + .map(|l| l.spans.iter().map(|s| s.content.clone()).collect::()) + .collect(); + let header_count2 = texts2.iter().filter(|s| s.contains("thinking")).count(); + assert_eq!(header_count2, 0, "header should not repeat after pause: {texts2:?}"); + assert!(texts2.iter().any(|s| s == "second"), "missing second line: {texts2:?}"); + + // Finalize; trailing blank should be added; no extra header. + w.handle_codex_event(Event { + id: "r1".into(), + msg: EventMsg::AgentReasoning(AgentReasoningEvent { text: String::new() }), + }); + // Drain remaining with ticks. + w.on_commit_tick(); + let lines3 = super::recv_insert_history(&rx, 200).expect("history after finalize tick"); + let texts3: Vec = lines3 + .iter() + .map(|l| l.spans.iter().map(|s| s.content.clone()).collect::()) + .collect(); + let header_total = texts1 + .into_iter() + .chain(texts2.into_iter()) + .chain(texts3.iter().cloned()) + .filter(|s| s.contains("thinking")) + .count(); + assert_eq!(header_total, 1, "header should appear exactly once across pauses and finalize"); + assert!(texts3.last().is_some_and(|s| s.is_empty()), "expected trailing blank line"); + } +} diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 443c54aa9b..3cbd39c1f1 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -1,5 +1,6 @@ use crate::exec_command::relativize_to_home; use crate::exec_command::strip_bash_lc_and_escape; +use crate::insert_history::word_wrap_lines; use crate::slash_command::SlashCommand; use crate::text_block::TextBlock; use crate::text_formatting::format_and_truncate_tool_result; @@ -30,7 +31,6 @@ use ratatui::text::Line as RtLine; use ratatui::text::Span as RtSpan; use ratatui::widgets::Paragraph; use ratatui::widgets::WidgetRef; -use ratatui::widgets::Wrap; use std::collections::HashMap; use std::io::Cursor; use std::path::PathBuf; @@ -187,11 +187,8 @@ impl HistoryCell { } pub(crate) fn desired_height(&self, width: u16) -> u16 { - Paragraph::new(Text::from(self.plain_lines())) - .wrap(Wrap { trim: false }) - .line_count(width) - .try_into() - .unwrap_or(0) + let wrapped = word_wrap_lines(&self.plain_lines(), width); + wrapped.len() as u16 } pub(crate) fn new_session_info( @@ -821,9 +818,8 @@ impl HistoryCell { impl WidgetRef for &HistoryCell { fn render_ref(&self, area: Rect, buf: &mut Buffer) { - Paragraph::new(Text::from(self.plain_lines())) - .wrap(Wrap { trim: false }) - .render(area, buf); + let wrapped = word_wrap_lines(&self.plain_lines(), area.width); + Paragraph::new(Text::from(wrapped)).render(area, buf); } } diff --git a/codex-rs/tui/src/insert_history.rs b/codex-rs/tui/src/insert_history.rs index 5c316637b1..971c376234 100644 --- a/codex-rs/tui/src/insert_history.rs +++ b/codex-rs/tui/src/insert_history.rs @@ -18,6 +18,8 @@ use ratatui::style::Color; use ratatui::style::Modifier; use ratatui::text::Line; use ratatui::text::Span; +use textwrap::Options as TwOptions; +use textwrap::WordSplitter; /// Insert `lines` above the viewport. pub(crate) fn insert_history_lines(terminal: &mut tui::Tui, lines: Vec) { @@ -40,7 +42,10 @@ pub fn insert_history_lines_to_writer( let mut area = terminal.get_frame().area(); - let wrapped_lines = wrapped_line_count(&lines, area.width); + // Pre-wrap lines using word-aware wrapping so terminal scrollback sees the same + // formatting as the TUI. This avoids character-level hard wrapping by the terminal. + let wrapped = word_wrap_lines(&lines, area.width.max(1)); + let wrapped_lines = wrapped.len() as u16; let cursor_top = if area.bottom() < screen_size.height { // If the viewport is not at the bottom of the screen, scroll it down to make room. // Don't scroll it past the bottom of the screen. @@ -91,7 +96,7 @@ pub fn insert_history_lines_to_writer( // fetch/restore the cursor position. insert_history_lines should be cursor-position-neutral :) queue!(writer, MoveTo(0, cursor_top)).ok(); - for line in lines { + for line in wrapped { queue!(writer, Print("\r\n")).ok(); write_spans(writer, line.iter()).ok(); } @@ -104,36 +109,6 @@ pub fn insert_history_lines_to_writer( } } -fn wrapped_line_count(lines: &[Line], width: u16) -> u16 { - let mut count = 0; - for line in lines { - count += line_height(line, width); - } - count -} - -fn line_height(line: &Line, width: u16) -> u16 { - // Use the same visible-width slicing semantics as the live row builder so - // our pre-scroll estimation matches how rows will actually wrap. - let w = width.max(1) as usize; - let mut rows = 0u16; - let mut remaining = line - .spans - .iter() - .map(|s| s.content.as_ref()) - .collect::>() - .join(""); - while !remaining.is_empty() { - let (_prefix, suffix, taken) = crate::live_wrap::take_prefix_by_width(&remaining, w); - rows = rows.saturating_add(1); - if taken >= remaining.len() { - break; - } - remaining = suffix.to_string(); - } - rows.max(1) -} - #[derive(Debug, Clone, PartialEq, Eq)] pub struct SetScrollRegion(pub std::ops::Range); @@ -282,6 +257,126 @@ where ) } +/// Word-aware wrapping for a list of `Line`s preserving styles. +pub(crate) fn word_wrap_lines(lines: &[Line], width: u16) -> Vec> { + let mut out = Vec::new(); + let w = width.max(1) as usize; + for line in lines { + out.extend(word_wrap_line(line, w)); + } + out +} + +fn word_wrap_line(line: &Line, width: usize) -> Vec> { + if width == 0 { + return vec![to_owned_line(line)]; + } + // Concatenate content and keep span boundaries for later re-slicing. + let mut flat = String::new(); + let mut span_bounds = Vec::new(); // (start_byte, end_byte, style) + let mut cursor = 0usize; + for s in &line.spans { + let text = s.content.as_ref(); + let start = cursor; + flat.push_str(text); + cursor += text.len(); + span_bounds.push((start, cursor, s.style)); + } + + // Use textwrap for robust word-aware wrapping; no hyphenation, no breaking words. + let opts = TwOptions::new(width) + .break_words(false) + .word_splitter(WordSplitter::NoHyphenation); + let wrapped = textwrap::wrap(&flat, &opts); + + if wrapped.len() <= 1 { + return vec![to_owned_line(line)]; + } + + // Map wrapped pieces back to byte ranges in `flat` sequentially. + let mut start_cursor = 0usize; + let mut out: Vec> = Vec::with_capacity(wrapped.len()); + for piece in wrapped { + let piece_str: &str = &piece; + if piece_str.is_empty() { + out.push(Line { + style: line.style, + alignment: line.alignment, + spans: Vec::new(), + }); + continue; + } + // Find the next occurrence of piece_str at or after start_cursor. + // textwrap preserves order, so a linear scan is sufficient. + if let Some(rel) = flat[start_cursor..].find(piece_str) { + let s = start_cursor + rel; + let e = s + piece_str.len(); + out.push(slice_line_spans(line, &span_bounds, s, e)); + start_cursor = e; + } else { + // Fallback: slice by length from cursor. + let s = start_cursor; + let e = (start_cursor + piece_str.len()).min(flat.len()); + out.push(slice_line_spans(line, &span_bounds, s, e)); + start_cursor = e; + } + } + + out +} + +fn to_owned_line(l: &Line<'_>) -> Line<'static> { + Line { + style: l.style, + alignment: l.alignment, + spans: l + .spans + .iter() + .map(|s| Span { + style: s.style, + content: std::borrow::Cow::Owned(s.content.to_string()), + }) + .collect(), + } +} + +fn slice_line_spans( + original: &Line<'_>, + span_bounds: &[(usize, usize, ratatui::style::Style)], + start_byte: usize, + end_byte: usize, +) -> Line<'static> { + let mut acc: Vec> = Vec::new(); + for (i, (s, e, style)) in span_bounds.iter().enumerate() { + if *e <= start_byte { + continue; + } + if *s >= end_byte { + break; + } + let seg_start = start_byte.max(*s); + let seg_end = end_byte.min(*e); + if seg_end > seg_start { + let local_start = seg_start - *s; + let local_end = seg_end - *s; + let content = original.spans[i].content.as_ref(); + let slice = &content[local_start..local_end]; + acc.push(Span { + style: *style, + content: std::borrow::Cow::Owned(slice.to_string()), + }); + } + if *e >= end_byte { + break; + } + } + Line { + style: original.style, + alignment: original.alignment, + spans: acc, + } +} + #[cfg(test)] mod tests { #![allow(clippy::unwrap_used)] @@ -318,8 +413,34 @@ mod tests { #[test] fn line_height_counts_double_width_emoji() { let line = Line::from("😀😀😀"); // each emoji ~ width 2 - assert_eq!(line_height(&line, 4), 2); - assert_eq!(line_height(&line, 2), 3); - assert_eq!(line_height(&line, 6), 1); + assert_eq!(word_wrap_line(&line, 4).len(), 2); + assert_eq!(word_wrap_line(&line, 2).len(), 3); + assert_eq!(word_wrap_line(&line, 6).len(), 1); + } + + #[test] + fn word_wrap_does_not_split_words_simple_english() { + let sample = "Years passed, and Willowmere thrived in peace and friendship. Mira’s herb garden flourished with both ordinary and enchanted plants, and travelers spoke of the kindness of the woman who tended them."; + let line = Line::from(sample); + // Force small width to exercise wrapping at spaces. + let wrapped = word_wrap_lines(&[line], 40); + let joined: String = wrapped + .iter() + .map(|l| { + l.spans + .iter() + .map(|s| s.content.clone()) + .collect::() + }) + .collect::>() + .join("\n"); + assert!( + !joined.contains("bo\nth"), + "word 'both' should not be split across lines:\n{joined}" + ); + assert!( + !joined.contains("Willowm\nere"), + "should not split inside words:\n{joined}" + ); } } diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index e15a235a71..056ece9feb 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -39,6 +39,7 @@ pub mod insert_history; pub mod live_wrap; mod log_layer; mod markdown; +mod markdown_stream; pub mod onboarding; mod shimmer; mod slash_command; @@ -55,6 +56,8 @@ use color_eyre::owo_colors::OwoColorize; pub use cli::Cli; +// (tests access modules directly within the crate) + pub async fn run_main( cli: Cli, codex_linux_sandbox_exe: Option, diff --git a/codex-rs/tui/src/markdown.rs b/codex-rs/tui/src/markdown.rs index 910a6869ec..124c7c06b2 100644 --- a/codex-rs/tui/src/markdown.rs +++ b/codex-rs/tui/src/markdown.rs @@ -22,35 +22,35 @@ fn append_markdown_with_opener_and_cwd( file_opener: UriBasedFileOpener, cwd: &Path, ) { - // Perform citation rewrite *before* feeding the string to the markdown - // renderer. When `file_opener` is absent we bypass the transformation to - // avoid unnecessary allocations. - let processed_markdown = rewrite_file_citations(markdown_source, file_opener, cwd); - - let markdown = tui_markdown::from_str(&processed_markdown); - - // `tui_markdown` returns a `ratatui::text::Text` where every `Line` borrows - // from the input `message` string. Since the `HistoryCell` stores its lines - // with a `'static` lifetime we must create an **owned** copy of each line - // so that it is no longer tied to `message`. We do this by cloning the - // content of every `Span` into an owned `String`. - - for borrowed_line in markdown.lines { - let mut owned_spans = Vec::with_capacity(borrowed_line.spans.len()); - for span in &borrowed_line.spans { - // Create a new owned String for the span's content to break the lifetime link. - let owned_span = Span::styled(span.content.to_string(), span.style); - owned_spans.push(owned_span); + // Historically, we fed the entire `markdown_source` into the renderer in + // one pass. However, fenced code blocks sometimes lost leading whitespace + // when formatted by the markdown renderer/highlighter. To preserve code + // block content exactly, split the source into "text" and "code" segments: + // - Render non-code text through `tui_markdown` (with citation rewrite). + // - Render code block content verbatim as plain lines without additional + // formatting, preserving leading spaces. + for seg in split_text_and_fences(markdown_source) { + match seg { + Segment::Text(s) => { + let processed = rewrite_file_citations(&s, file_opener, cwd); + let rendered = tui_markdown::from_str(&processed); + push_owned_lines(rendered.lines, lines); + } + Segment::Code { content, .. } => { + // Emit the code content exactly as-is, line by line. + // We don't attempt syntax highlighting to avoid whitespace bugs. + for line in content.split_inclusive('\n') { + // split_inclusive keeps the trailing \n; we want lines without it. + let line = if let Some(stripped) = line.strip_suffix('\n') { + stripped + } else { + line + }; + let owned_line: Line<'static> = Line::from(Span::raw(line.to_string())); + lines.push(owned_line); + } + } } - - let owned_line: Line<'static> = Line::from(owned_spans).style(borrowed_line.style); - // Preserve alignment if it was set on the source line. - let owned_line = match borrowed_line.alignment { - Some(alignment) => owned_line.alignment(alignment), - None => owned_line, - }; - - lines.push(owned_line); } } @@ -101,6 +101,177 @@ fn rewrite_file_citations<'a>( }) } +// Helper to clone borrowed ratatui lines into owned lines with 'static lifetime. +fn push_owned_lines<'a>(borrowed: Vec>, out: &mut Vec>) { + for borrowed_line in borrowed { + let mut owned_spans = Vec::with_capacity(borrowed_line.spans.len()); + for span in &borrowed_line.spans { + let owned_span = Span::styled(span.content.to_string(), span.style); + owned_spans.push(owned_span); + } + let owned_line: Line<'static> = Line::from(owned_spans).style(borrowed_line.style); + let owned_line = match borrowed_line.alignment { + Some(alignment) => owned_line.alignment(alignment), + None => owned_line, + }; + out.push(owned_line); + } +} + +// Minimal code block splitting. +// - Recognizes fenced blocks opened by ``` or ~~~ (allowing leading whitespace). +// The opening fence may include a language string which we ignore. +// The closing fence must be on its own line (ignoring surrounding whitespace). +// - Additionally recognizes indented code blocks that begin after a blank line +// with a line starting with at least 4 spaces or a tab, and continue for +// consecutive lines that are blank or also indented by >= 4 spaces or a tab. +enum Segment { + Text(String), + Code { + _lang: Option, + content: String, + }, +} + +fn split_text_and_fences(src: &str) -> Vec { + let mut segments = Vec::new(); + let mut curr_text = String::new(); + #[derive(Copy, Clone, PartialEq)] + enum CodeMode { + None, + Fenced, + Indented, + } + let mut code_mode = CodeMode::None; + let mut fence_token = ""; + let mut code_lang: Option = None; + let mut code_content = String::new(); + // We intentionally do not require a preceding blank line for indented code blocks, + // since streamed model output often omits it. This favors preserving indentation. + + for line in src.split_inclusive('\n') { + let line_no_nl = line.strip_suffix('\n'); + let trimmed_start = match line_no_nl { + Some(l) => l.trim_start(), + None => line.trim_start(), + }; + if code_mode == CodeMode::None { + let open = if trimmed_start.starts_with("```") { + Some("```") + } else if trimmed_start.starts_with("~~~") { + Some("~~~") + } else { + None + }; + if let Some(tok) = open { + // Flush pending text segment. + if !curr_text.is_empty() { + segments.push(Segment::Text(curr_text.clone())); + curr_text.clear(); + } + fence_token = tok; + // Capture language after the token on this line (before newline). + let after = &trimmed_start[tok.len()..]; + let lang = after.trim(); + code_lang = if lang.is_empty() { + None + } else { + Some(lang.to_string()) + }; + code_mode = CodeMode::Fenced; + code_content.clear(); + // Do not include the opening fence line in output. + continue; + } + // Check for start of an indented code block: only after a blank line + // (or at the beginning), and the line must start with >=4 spaces or a tab. + let raw_line = match line_no_nl { + Some(l) => l, + None => line, + }; + let leading_spaces = raw_line.chars().take_while(|c| *c == ' ').count(); + let starts_with_tab = raw_line.starts_with('\t'); + // Consider any line that begins with >=4 spaces or a tab to start an + // indented code block. This favors preserving indentation even when a + // preceding blank line is omitted (common in streamed model output). + let starts_indented_code = (leading_spaces >= 4) || starts_with_tab; + if starts_indented_code { + // Flush pending text and begin an indented code block. + if !curr_text.is_empty() { + segments.push(Segment::Text(curr_text.clone())); + curr_text.clear(); + } + code_mode = CodeMode::Indented; + code_content.clear(); + code_content.push_str(line); + // Inside code now; do not treat this line as normal text. + continue; + } + // Normal text line. + curr_text.push_str(line); + } else { + match code_mode { + CodeMode::Fenced => { + // inside fenced code: check for closing fence on its own line + let trimmed = match line_no_nl { + Some(l) => l.trim(), + None => line.trim(), + }; + if trimmed == fence_token { + // End code block: emit segment without fences + segments.push(Segment::Code { + _lang: code_lang.take(), + content: code_content.clone(), + }); + code_content.clear(); + code_mode = CodeMode::None; + fence_token = ""; + continue; + } + // Accumulate code content exactly as-is. + code_content.push_str(line); + } + CodeMode::Indented => { + // Continue while the line is blank, or starts with >=4 spaces, or a tab. + let raw_line = match line_no_nl { + Some(l) => l, + None => line, + }; + let is_blank = raw_line.trim().is_empty(); + let leading_spaces = raw_line.chars().take_while(|c| *c == ' ').count(); + let starts_with_tab = raw_line.starts_with('\t'); + if is_blank || leading_spaces >= 4 || starts_with_tab { + code_content.push_str(line); + } else { + // Close the indented code block and reprocess this line as normal text. + segments.push(Segment::Code { + _lang: None, + content: code_content.clone(), + }); + code_content.clear(); + code_mode = CodeMode::None; + // Now handle current line as text. + curr_text.push_str(line); + } + } + CodeMode::None => unreachable!(), + } + } + } + + if code_mode != CodeMode::None { + // Unterminated code fence: treat accumulated content as a code segment. + segments.push(Segment::Code { + _lang: code_lang.take(), + content: code_content.clone(), + }); + } else if !curr_text.is_empty() { + segments.push(Segment::Text(curr_text.clone())); + } + + segments +} + #[cfg(test)] mod tests { use super::*; @@ -162,4 +333,99 @@ mod tests { // Ensure helper rewrites. assert_ne!(markdown, unchanged); } + + #[test] + fn fenced_code_blocks_preserve_leading_whitespace() { + let src = "```\n indented\n\t\twith tabs\n four spaces\n```\n"; + let cwd = Path::new("/"); + let mut out = Vec::new(); + append_markdown_with_opener_and_cwd(src, &mut out, UriBasedFileOpener::None, cwd); + let rendered: Vec = out + .iter() + .map(|l| { + l.spans + .iter() + .map(|s| s.content.clone()) + .collect::() + }) + .collect(); + assert_eq!( + rendered, + vec![ + " indented".to_string(), + "\t\twith tabs".to_string(), + " four spaces".to_string() + ] + ); + } + + #[test] + fn citations_not_rewritten_inside_code_blocks() { + let src = "Before 【F:/x.rs†L1】\n```\nInside 【F:/x.rs†L2】\n```\nAfter 【F:/x.rs†L3】\n"; + let cwd = Path::new("/"); + let mut out = Vec::new(); + append_markdown_with_opener_and_cwd(src, &mut out, UriBasedFileOpener::VsCode, cwd); + let rendered: Vec = out + .iter() + .map(|l| { + l.spans + .iter() + .map(|s| s.content.clone()) + .collect::() + }) + .collect(); + // Expect first and last lines rewritten, middle line unchanged. + assert!(rendered[0].contains("vscode://file")); + assert_eq!(rendered[1], "Inside 【F:/x.rs†L2】"); + assert!(matches!(rendered.last(), Some(s) if s.contains("vscode://file"))); + } + + #[test] + fn indented_code_blocks_preserve_leading_whitespace() { + let src = "Before\n code 1\n\tcode with tab\n code 2\nAfter\n"; + let cwd = Path::new("/"); + let mut out = Vec::new(); + append_markdown_with_opener_and_cwd(src, &mut out, UriBasedFileOpener::None, cwd); + let rendered: Vec = out + .iter() + .map(|l| { + l.spans + .iter() + .map(|s| s.content.clone()) + .collect::() + }) + .collect(); + assert_eq!( + rendered, + vec![ + "Before".to_string(), + " code 1".to_string(), + "\tcode with tab".to_string(), + " code 2".to_string(), + "After".to_string() + ] + ); + } + + #[test] + fn citations_not_rewritten_inside_indented_code_blocks() { + let src = "Start 【F:/x.rs†L1】\n\n Inside 【F:/x.rs†L2】\n\nEnd 【F:/x.rs†L3】\n"; + let cwd = Path::new("/"); + let mut out = Vec::new(); + append_markdown_with_opener_and_cwd(src, &mut out, UriBasedFileOpener::VsCode, cwd); + let rendered: Vec = out + .iter() + .map(|l| { + l.spans + .iter() + .map(|s| s.content.clone()) + .collect::() + }) + .collect(); + // Expect first and last lines rewritten, and the indented code line present + // unchanged (citations inside not rewritten). We do not assert on blank + // separator lines since the markdown renderer may normalize them. + assert!(rendered.iter().any(|s| s.contains("vscode://file"))); + assert!(rendered.iter().any(|s| s == " Inside 【F:/x.rs†L2】")); + } } diff --git a/codex-rs/tui/src/markdown_stream.rs b/codex-rs/tui/src/markdown_stream.rs new file mode 100644 index 0000000000..9eeb1740a1 --- /dev/null +++ b/codex-rs/tui/src/markdown_stream.rs @@ -0,0 +1,565 @@ +use std::collections::VecDeque; + +use codex_core::config::Config; +use ratatui::text::Line; + +use crate::markdown; + +/// Newline-gated accumulator that renders markdown and commits only fully +/// completed logical lines. +pub(crate) struct MarkdownNewlineCollector { + buffer: String, + committed_line_count: usize, +} + +impl MarkdownNewlineCollector { + pub fn new() -> Self { + Self { + buffer: String::new(), + committed_line_count: 0, + } + } + + pub fn clear(&mut self) { + self.buffer.clear(); + self.committed_line_count = 0; + } + + pub fn push_delta(&mut self, delta: &str) { + self.buffer.push_str(delta); + } + + /// Render the full buffer and return only the newly completed logical lines + /// since the last commit. When the buffer does not end with a newline, the + /// final rendered line is considered incomplete and is not emitted. + pub fn commit_complete_lines(&mut self, config: &Config) -> Vec> { + // In non-test builds, unwrap an outer ```markdown fence during commit as well, + // so fence markers never appear in streamed history. + let source = unwrap_markdown_language_fence_if_enabled(self.buffer.clone()); + let source = strip_empty_fenced_code_blocks(&source); + + let mut rendered: Vec> = Vec::new(); + markdown::append_markdown(&source, &mut rendered, config); + + let mut complete_line_count = rendered.len(); + if complete_line_count > 0 && is_effectively_empty(&rendered[complete_line_count - 1]) { + complete_line_count -= 1; + } + if !self.buffer.ends_with('\n') { + complete_line_count = complete_line_count.saturating_sub(1); + // If we're inside an unclosed fenced code block, also drop the + // last rendered line to avoid committing a partial code line. + if is_inside_unclosed_fence(&source) { + complete_line_count = complete_line_count.saturating_sub(1); + } + } + + if self.committed_line_count >= complete_line_count { + return Vec::new(); + } + + let out_slice = &rendered[self.committed_line_count..complete_line_count]; + // Strong correctness: while a fenced code block is open (no closing fence yet), + // do not emit any new lines from inside it. Wait until the fence closes to emit + // the entire block together. This avoids stray backticks and misformatted content. + if is_inside_unclosed_fence(&source) { + return Vec::new(); + } + + let out = out_slice.to_vec(); + self.committed_line_count = complete_line_count; + out + } + + /// Finalize the stream: emit all remaining lines beyond the last commit. + /// If the buffer does not end with a newline, a temporary one is appended + /// for rendering. Optionally unwraps ```markdown language fences in + /// non-test builds. + pub fn finalize_and_drain(&mut self, config: &Config) -> Vec> { + let mut source: String = self.buffer.clone(); + if !source.ends_with('\n') { + source.push('\n'); + } + let source = unwrap_markdown_language_fence_if_enabled(source); + let source = strip_empty_fenced_code_blocks(&source); + + let mut rendered: Vec> = Vec::new(); + markdown::append_markdown(&source, &mut rendered, config); + + let out = if self.committed_line_count >= rendered.len() { + Vec::new() + } else { + rendered[self.committed_line_count..].to_vec() + }; + + // Reset collector state for next stream. + self.clear(); + out + } +} + +fn is_effectively_empty(line: &Line<'_>) -> bool { + if line.spans.is_empty() { + return true; + } + line.spans + .iter() + .all(|s| s.content.is_empty() || s.content.chars().all(|c| c == ' ')) +} + +/// Remove fenced code blocks that contain no content (whitespace-only) to avoid +/// streaming empty code blocks like ```lang\n``` or ```\n```. +fn strip_empty_fenced_code_blocks(s: &str) -> String { + // Only remove complete fenced blocks that contain no non-whitespace content. + // Leave all other content unchanged to avoid affecting partial streams. + let lines: Vec<&str> = s.lines().collect(); + let mut out = String::with_capacity(s.len()); + let mut i = 0usize; + while i < lines.len() { + let line = lines[i]; + let trimmed_start = line.trim_start(); + let fence_token = if trimmed_start.starts_with("```") { + "```" + } else if trimmed_start.starts_with("~~~") { + "~~~" + } else { + "" + }; + if !fence_token.is_empty() { + // Find a matching closing fence on its own line. + let mut j = i + 1; + let mut has_content = false; + let mut found_close = false; + while j < lines.len() { + let l = lines[j]; + if l.trim() == fence_token { + found_close = true; + break; + } + if !l.trim().is_empty() { + has_content = true; + } + j += 1; + } + if found_close && !has_content { + // Drop i..=j and insert at most a single blank separator line. + if !out.ends_with('\n') { + out.push('\n'); + } + i = j + 1; + continue; + } + // Not an empty fenced block; emit as-is. + out.push_str(line); + out.push('\n'); + i += 1; + } else { + out.push_str(line); + out.push('\n'); + i += 1; + } + } + out +} + +fn is_inside_unclosed_fence(s: &str) -> bool { + let mut open = false; + for line in s.lines() { + let t = line.trim_start(); + if t.starts_with("```") || t.starts_with("~~~") { + if !open { + open = true; + } else { + // closing fence on same pattern toggles off + open = false; + } + } + } + open +} + +#[cfg(test)] +fn unwrap_markdown_language_fence_if_enabled(s: String) -> String { + // In tests, keep content exactly as provided to simplify assertions. + s +} + +#[cfg(not(test))] +fn unwrap_markdown_language_fence_if_enabled(s: String) -> String { + // Best-effort unwrap of a single outer ```markdown fence. + // This is intentionally simple; we can refine as needed later. + const OPEN: &str = "```markdown\n"; + const CLOSE: &str = "\n```\n"; + if s.starts_with(OPEN) && s.ends_with(CLOSE) { + let inner = s[OPEN.len()..s.len() - CLOSE.len()].to_string(); + return inner; + } + s +} + +pub(crate) struct StepResult { + pub history: Vec>, // lines to insert into history this step +} + +/// Streams already-rendered rows into history while computing the newest K +/// rows to show in a live overlay. +pub(crate) struct RenderedLineStreamer { + queue: VecDeque>, +} + +impl RenderedLineStreamer { + pub fn new() -> Self { + Self { + queue: VecDeque::new(), + } + } + + pub fn clear(&mut self) { + self.queue.clear(); + } + + pub fn enqueue(&mut self, lines: Vec>) { + for l in lines { + self.queue.push_back(l); + } + } + + pub fn step(&mut self, _live_max_rows: usize) -> StepResult { + let mut history = Vec::new(); + // Move exactly one per tick to animate gradual insertion. + let burst = if self.queue.is_empty() { 0 } else { 1 }; + for _ in 0..burst { + if let Some(l) = self.queue.pop_front() { + history.push(l); + } + } + + StepResult { history } + } + + pub fn drain_all(&mut self, _live_max_rows: usize) -> StepResult { + let mut history = Vec::new(); + while let Some(l) = self.queue.pop_front() { + history.push(l); + } + StepResult { history } + } + + pub fn is_idle(&self) -> bool { + self.queue.is_empty() + } +} + +#[cfg(test)] +pub(crate) fn simulate_stream_markdown_for_tests( + deltas: &[&str], + finalize: bool, + config: &Config, +) -> Vec> { + let mut collector = MarkdownNewlineCollector::new(); + let mut out = Vec::new(); + for d in deltas { + collector.push_delta(d); + if d.contains('\n') { + out.extend(collector.commit_complete_lines(config)); + } + } + if finalize { + out.extend(collector.finalize_and_drain(config)); + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + use codex_core::config::Config; + use codex_core::config::ConfigOverrides; + + fn test_config() -> Config { + let overrides = ConfigOverrides { + cwd: std::env::current_dir().ok(), + ..Default::default() + }; + match Config::load_with_cli_overrides(vec![], overrides) { + Ok(c) => c, + Err(e) => panic!("load test config: {e}"), + } + } + + #[test] + fn no_commit_until_newline() { + let cfg = test_config(); + let mut c = MarkdownNewlineCollector::new(); + c.push_delta("Hello, world"); + let out = c.commit_complete_lines(&cfg); + assert!(out.is_empty(), "should not commit without newline"); + c.push_delta("!\n"); + let out2 = c.commit_complete_lines(&cfg); + assert_eq!(out2.len(), 1, "one completed line after newline"); + } + + #[test] + fn finalize_commits_partial_line() { + let cfg = test_config(); + let mut c = MarkdownNewlineCollector::new(); + c.push_delta("Line without newline"); + let out = c.finalize_and_drain(&cfg); + assert_eq!(out.len(), 1); + } + + #[test] + fn heading_starts_on_new_line_when_following_paragraph() { + let cfg = test_config(); + + // Stream a paragraph line, then a heading on the next line. + // Expect two distinct rendered lines: "Hello." and "Heading". + let mut c = MarkdownNewlineCollector::new(); + c.push_delta("Hello.\n"); + let out1 = c.commit_complete_lines(&cfg); + let s1: Vec = out1 + .iter() + .map(|l| { + l.spans + .iter() + .map(|s| s.content.clone()) + .collect::>() + .join("") + }) + .collect(); + assert_eq!( + out1.len(), + 1, + "first commit should contain only the paragraph line, got {}: {:?}", + out1.len(), + s1 + ); + + c.push_delta("## Heading\n"); + let out2 = c.commit_complete_lines(&cfg); + let s2: Vec = out2 + .iter() + .map(|l| { + l.spans + .iter() + .map(|s| s.content.clone()) + .collect::>() + .join("") + }) + .collect(); + assert_eq!( + s2, + vec!["", "## Heading"], + "expected a blank separator then the heading line" + ); + + let line_to_string = |l: &ratatui::text::Line<'_>| -> String { + l.spans + .iter() + .map(|s| s.content.clone()) + .collect::>() + .join("") + }; + + assert_eq!(line_to_string(&out1[0]), "Hello."); + assert_eq!(line_to_string(&out2[1]), "## Heading"); + } + + #[test] + fn heading_not_inlined_when_split_across_chunks() { + let cfg = test_config(); + + // Paragraph without trailing newline, then a chunk that starts with the newline + // and the heading text, then a final newline. The collector should first commit + // only the paragraph line, and later commit the heading as its own line. + let mut c = MarkdownNewlineCollector::new(); + c.push_delta("Sounds good!"); + // No commit yet + assert!(c.commit_complete_lines(&cfg).is_empty()); + + // Introduce the newline that completes the paragraph and the start of the heading. + c.push_delta("\n## Adding Bird subcommand"); + let out1 = c.commit_complete_lines(&cfg); + let s1: Vec = out1 + .iter() + .map(|l| { + l.spans + .iter() + .map(|s| s.content.clone()) + .collect::>() + .join("") + }) + .collect(); + assert_eq!( + s1, + vec!["Sounds good!", ""], + "expected paragraph followed by blank separator before heading chunk" + ); + + // Now finish the heading line with the trailing newline. + c.push_delta("\n"); + let out2 = c.commit_complete_lines(&cfg); + let s2: Vec = out2 + .iter() + .map(|l| { + l.spans + .iter() + .map(|s| s.content.clone()) + .collect::>() + .join("") + }) + .collect(); + assert_eq!( + s2, + vec!["## Adding Bird subcommand"], + "expected the heading line only on the final commit" + ); + + // Sanity check raw markdown rendering for a simple line does not produce spurious extras. + let mut rendered: Vec> = Vec::new(); + crate::markdown::append_markdown("Hello.\n", &mut rendered, &cfg); + let rendered_strings: Vec = rendered + .iter() + .map(|l| { + l.spans + .iter() + .map(|s| s.content.clone()) + .collect::>() + .join("") + }) + .collect(); + assert_eq!( + rendered_strings, + vec!["Hello."], + "unexpected markdown lines: {rendered_strings:?}" + ); + + let line_to_string = |l: &ratatui::text::Line<'_>| -> String { + l.spans + .iter() + .map(|s| s.content.clone()) + .collect::>() + .join("") + }; + + assert_eq!(line_to_string(&out1[0]), "Sounds good!"); + assert_eq!(line_to_string(&out1[1]), ""); + assert_eq!(line_to_string(&out2[0]), "## Adding Bird subcommand"); + } + + fn lines_to_plain_strings(lines: &[ratatui::text::Line<'_>]) -> Vec { + lines + .iter() + .map(|l| { + l.spans + .iter() + .map(|s| s.content.clone()) + .collect::>() + .join("") + }) + .collect() + } + + #[test] + fn lists_and_fences_commit_without_duplication() { + let cfg = test_config(); + + // List case + let deltas = vec!["- a\n- ", "b\n- c\n"]; + let streamed = simulate_stream_markdown_for_tests(&deltas, true, &cfg); + let streamed_str = lines_to_plain_strings(&streamed); + + let mut rendered_all: Vec> = Vec::new(); + crate::markdown::append_markdown("- a\n- b\n- c\n", &mut rendered_all, &cfg); + let rendered_all_str = lines_to_plain_strings(&rendered_all); + + assert_eq!( + streamed_str, rendered_all_str, + "list streaming should equal full render without duplication" + ); + + // Fenced code case: stream in small chunks + let deltas2 = vec!["```", "\nco", "de 1\ncode 2\n", "```\n"]; + let streamed2 = simulate_stream_markdown_for_tests(&deltas2, true, &cfg); + let streamed2_str = lines_to_plain_strings(&streamed2); + + let mut rendered_all2: Vec> = Vec::new(); + crate::markdown::append_markdown("```\ncode 1\ncode 2\n```\n", &mut rendered_all2, &cfg); + let rendered_all2_str = lines_to_plain_strings(&rendered_all2); + + assert_eq!( + streamed2_str, rendered_all2_str, + "fence streaming should equal full render without duplication" + ); + } + + #[test] + fn utf8_boundary_safety_and_wide_chars() { + let cfg = test_config(); + + // Emoji (wide), CJK, control char, digit + combining macron sequences + let input = "🙂🙂🙂\n汉字漢字\nA\u{0003}0\u{0304}\n"; + let deltas = vec![ + "🙂", + "🙂", + "🙂\n汉", + "字漢", + "字\nA", + "\u{0003}", + "0", + "\u{0304}", + "\n", + ]; + + let streamed = simulate_stream_markdown_for_tests(&deltas, true, &cfg); + let streamed_str = lines_to_plain_strings(&streamed); + + let mut rendered_all: Vec> = Vec::new(); + crate::markdown::append_markdown(input, &mut rendered_all, &cfg); + let rendered_all_str = lines_to_plain_strings(&rendered_all); + + assert_eq!( + streamed_str, rendered_all_str, + "utf8/wide-char streaming should equal full render without duplication or truncation" + ); + } + + #[test] + fn empty_fenced_block_is_dropped_and_separator_preserved_before_heading() { + let cfg = test_config(); + // An empty fenced code block followed by a heading should not render the fence, + // but should preserve a blank separator line so the heading starts on a new line. + let deltas = vec!["```bash\n```\n", "## Heading\n"]; // empty block and close in same commit + let streamed = simulate_stream_markdown_for_tests(&deltas, true, &cfg); + let texts = lines_to_plain_strings(&streamed); + assert!( + texts.iter().all(|s| !s.contains("```")), + "no fence markers expected: {texts:?}" + ); + // Expect the heading and no fence markers. A blank separator may or may not be rendered at start. + assert!( + texts.iter().any(|s| s == "## Heading"), + "expected heading line: {texts:?}" + ); + } + + #[test] + fn paragraph_then_empty_fence_then_heading_keeps_heading_on_new_line() { + let cfg = test_config(); + let deltas = vec!["Para.\n", "```\n```\n", "## Title\n"]; // empty fence block in one commit + let streamed = simulate_stream_markdown_for_tests(&deltas, true, &cfg); + let texts = lines_to_plain_strings(&streamed); + let para_idx = match texts.iter().position(|s| s == "Para.") { + Some(i) => i, + None => panic!("para present"), + }; + let head_idx = match texts.iter().position(|s| s == "## Title") { + Some(i) => i, + None => panic!("heading present"), + }; + assert!( + head_idx > para_idx, + "heading should not merge with paragraph: {texts:?}" + ); + } +} diff --git a/codex-rs/tui/tests/vt100_history.rs b/codex-rs/tui/tests/vt100_history.rs index 11ee044041..402e847b47 100644 --- a/codex-rs/tui/tests/vt100_history.rs +++ b/codex-rs/tui/tests/vt100_history.rs @@ -75,7 +75,7 @@ impl TestScenario { } #[test] -fn hist_001_basic_insertion_no_wrap() { +fn basic_insertion_no_wrap() { // Screen of 20x6; viewport is the last row (height=1 at y=5) let area = Rect::new(0, 5, 20, 1); let mut scenario = TestScenario::new(20, 6, area); @@ -97,7 +97,7 @@ fn hist_001_basic_insertion_no_wrap() { } #[test] -fn hist_002_long_token_wraps() { +fn long_token_wraps() { let area = Rect::new(0, 5, 20, 1); let mut scenario = TestScenario::new(20, 6, area); @@ -130,7 +130,7 @@ fn hist_002_long_token_wraps() { } #[test] -fn hist_003_emoji_and_cjk() { +fn emoji_and_cjk() { let area = Rect::new(0, 5, 20, 1); let mut scenario = TestScenario::new(20, 6, area); @@ -148,7 +148,7 @@ fn hist_003_emoji_and_cjk() { } #[test] -fn hist_004_mixed_ansi_spans() { +fn mixed_ansi_spans() { let area = Rect::new(0, 5, 20, 1); let mut scenario = TestScenario::new(20, 6, area); @@ -162,7 +162,7 @@ fn hist_004_mixed_ansi_spans() { } #[test] -fn hist_006_cursor_restoration() { +fn cursor_restoration() { let area = Rect::new(0, 5, 20, 1); let mut scenario = TestScenario::new(20, 6, area); @@ -182,7 +182,39 @@ fn hist_006_cursor_restoration() { } #[test] -fn hist_005_pre_scroll_region_down() { +fn word_wrap_no_mid_word_split() { + // Screen of 40x10; viewport is the last row + let area = Rect::new(0, 9, 40, 1); + let mut scenario = TestScenario::new(40, 10, area); + + let sample = "Years passed, and Willowmere thrived in peace and friendship. Mira’s herb garden flourished with both ordinary and enchanted plants, and travelers spoke of the kindness of the woman who tended them."; + let buf = scenario.run_insert(vec![Line::from(sample)]); + let rows = scenario.screen_rows_from_bytes(&buf); + let joined = rows.join("\n"); + assert!( + !joined.contains("bo\nth"), + "word 'both' should not be split across lines:\n{joined}" + ); +} + +#[test] +fn em_dash_and_space_word_wrap() { + // Repro from report: ensure we break before "inside", not mid-word. + let area = Rect::new(0, 9, 40, 1); + let mut scenario = TestScenario::new(40, 10, area); + + let sample = "Mara found an old key on the shore. Curious, she opened a tarnished box half-buried in sand—and inside lay a single, glowing seed."; + let buf = scenario.run_insert(vec![Line::from(sample)]); + let rows = scenario.screen_rows_from_bytes(&buf); + let joined = rows.join("\n"); + assert!( + !joined.contains("insi\nde"), + "word 'inside' should not be split across lines:\n{joined}" + ); +} + +#[test] +fn pre_scroll_region_down() { // Viewport not at bottom: y=3 (0-based), height=1 let area = Rect::new(0, 3, 20, 1); let mut scenario = TestScenario::new(20, 6, area); diff --git a/codex-rs/tui/tests/vt100_streaming_no_dup.rs b/codex-rs/tui/tests/vt100_streaming_no_dup.rs new file mode 100644 index 0000000000..a359e77a08 --- /dev/null +++ b/codex-rs/tui/tests/vt100_streaming_no_dup.rs @@ -0,0 +1,77 @@ +#![cfg(feature = "vt100-tests")] + +use ratatui::backend::TestBackend; +use ratatui::layout::Rect; +use ratatui::text::Line; + +fn term(viewport: Rect) -> codex_tui::custom_terminal::Terminal { + let backend = TestBackend::new(20, 6); + let mut term = codex_tui::custom_terminal::Terminal::with_options(backend) + .unwrap_or_else(|e| panic!("failed to construct terminal: {e}")); + term.set_viewport_area(viewport); + term +} + +#[test] +fn stream_commit_trickle_no_duplication() { + // Viewport is the last row (height=1 at y=5) + let area = Rect::new(0, 5, 20, 1); + let mut t = term(area); + + // Step 1: commit first row + let mut out1 = Vec::new(); + codex_tui::insert_history::insert_history_lines_to_writer( + &mut t, + &mut out1, + vec![Line::from("one")], + ); + + // Step 2: later commit next row + let mut out2 = Vec::new(); + codex_tui::insert_history::insert_history_lines_to_writer( + &mut t, + &mut out2, + vec![Line::from("two")], + ); + + let combined = [out1, out2].concat(); + let s = String::from_utf8_lossy(&combined); + assert_eq!( + s.matches("one").count(), + 1, + "history line duplicated: {s:?}" + ); + assert_eq!( + s.matches("two").count(), + 1, + "history line duplicated: {s:?}" + ); + assert!( + !s.contains("three"), + "live-only content leaked into history: {s:?}" + ); +} + +#[test] +fn live_ring_rows_not_inserted_into_history() { + let area = Rect::new(0, 5, 20, 1); + let mut t = term(area); + + // Commit two rows to history. + let mut buf = Vec::new(); + codex_tui::insert_history::insert_history_lines_to_writer( + &mut t, + &mut buf, + vec![Line::from("one"), Line::from("two")], + ); + + // The live ring might display tail+head rows like ["two", "three"], + // but only committed rows should be present in the history ANSI stream. + let s = String::from_utf8_lossy(&buf); + assert!(s.contains("one")); + assert!(s.contains("two")); + assert!( + !s.contains("three"), + "uncommitted live-ring content should not be inserted into history: {s:?}" + ); +} From 52e12f2b6cd730780f976d72c044593709aabec9 Mon Sep 17 00:00:00 2001 From: easong-openai Date: Thu, 7 Aug 2025 18:38:39 -0700 Subject: [PATCH 0101/1309] Revert "Streaming markdown (#1920)" (#1981) This reverts commit 2b7139859ec1edcdfe271b1f7615f308f8e60a53. --- codex-rs/core/src/client.rs | 12 +- codex-rs/tui/src/app.rs | 32 +- codex-rs/tui/src/app_event.rs | 4 - .../tui/src/bottom_pane/live_ring_widget.rs | 45 ++ codex-rs/tui/src/bottom_pane/mod.rs | 152 ++++- codex-rs/tui/src/chatwidget.rs | 568 +++++++----------- codex-rs/tui/src/chatwidget_stream_tests.rs | 392 ------------ codex-rs/tui/src/history_cell.rs | 14 +- codex-rs/tui/src/insert_history.rs | 191 ++---- codex-rs/tui/src/lib.rs | 3 - codex-rs/tui/src/markdown.rs | 322 +--------- codex-rs/tui/src/markdown_stream.rs | 565 ----------------- codex-rs/tui/tests/vt100_history.rs | 44 +- codex-rs/tui/tests/vt100_streaming_no_dup.rs | 77 --- 14 files changed, 481 insertions(+), 1940 deletions(-) create mode 100644 codex-rs/tui/src/bottom_pane/live_ring_widget.rs delete mode 100644 codex-rs/tui/src/chatwidget_stream_tests.rs delete mode 100644 codex-rs/tui/src/markdown_stream.rs delete mode 100644 codex-rs/tui/tests/vt100_streaming_no_dup.rs diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index d19f73d6e0..0caf1170a6 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -504,17 +504,11 @@ async fn process_sse( | "response.in_progress" | "response.output_item.added" | "response.output_text.done" - | "response.reasoning_summary_part.added" => { - // Currently, we ignore this event, but we handle it + | "response.reasoning_summary_part.added" + | "response.reasoning_summary_text.done" => { + // Currently, we ignore these events, but we handle them // separately to skip the logging message in the `other` case. } - "response.reasoning_summary_text.done" => { - // End reasoning summary with a blank separator. - let event = ResponseEvent::ReasoningSummaryDelta("\n\n".to_string()); - if tx_event.send(Ok(event)).await.is_err() { - return; - } - } other => debug!(other, "sse event"), } } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 5d189e91bb..86d7414151 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -64,9 +64,6 @@ pub(crate) struct App<'a> { pending_history_lines: Vec>, enhanced_keys_supported: bool, - - /// Controls the animation thread that sends CommitTick events. - commit_anim_running: Arc, } /// Aggregate parameters needed to create a `ChatWidget`, as creation may be @@ -176,7 +173,6 @@ impl App<'_> { file_search, pending_redraw, enhanced_keys_supported, - commit_anim_running: Arc::new(AtomicBool::new(false)), } } @@ -193,7 +189,7 @@ impl App<'_> { // redraw is already pending so we can return early. if self .pending_redraw - .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) .is_err() { return; @@ -204,7 +200,7 @@ impl App<'_> { thread::spawn(move || { thread::sleep(REDRAW_DEBOUNCE); tx.send(AppEvent::Redraw); - pending_redraw.store(false, Ordering::Release); + pending_redraw.store(false, Ordering::SeqCst); }); } @@ -225,30 +221,6 @@ impl App<'_> { AppEvent::Redraw => { std::io::stdout().sync_update(|_| self.draw_next_frame(terminal))??; } - AppEvent::StartCommitAnimation => { - if self - .commit_anim_running - .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) - .is_ok() - { - let tx = self.app_event_tx.clone(); - let running = self.commit_anim_running.clone(); - thread::spawn(move || { - while running.load(Ordering::Relaxed) { - thread::sleep(Duration::from_millis(50)); - tx.send(AppEvent::CommitTick); - } - }); - } - } - AppEvent::StopCommitAnimation => { - self.commit_anim_running.store(false, Ordering::Release); - } - AppEvent::CommitTick => { - if let AppState::Chat { widget } = &mut self.app_state { - widget.on_commit_tick(); - } - } AppEvent::KeyEvent(key_event) => { match key_event { KeyEvent { diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 9965a91ebc..7f96fe1e47 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -50,10 +50,6 @@ pub(crate) enum AppEvent { InsertHistory(Vec>), - StartCommitAnimation, - StopCommitAnimation, - CommitTick, - /// Onboarding: result of login_with_chatgpt. OnboardingAuthComplete(Result<(), String>), OnboardingComplete(ChatWidgetArgs), diff --git a/codex-rs/tui/src/bottom_pane/live_ring_widget.rs b/codex-rs/tui/src/bottom_pane/live_ring_widget.rs new file mode 100644 index 0000000000..13f91acc5d --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/live_ring_widget.rs @@ -0,0 +1,45 @@ +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::text::Line; +use ratatui::widgets::Paragraph; +use ratatui::widgets::WidgetRef; + +/// Minimal rendering-only widget for the transient ring rows. +pub(crate) struct LiveRingWidget { + max_rows: u16, + rows: Vec>, // newest at the end +} + +impl LiveRingWidget { + pub fn new() -> Self { + Self { + max_rows: 3, + rows: Vec::new(), + } + } + + pub fn set_max_rows(&mut self, n: u16) { + self.max_rows = n.max(1); + } + + pub fn set_rows(&mut self, rows: Vec>) { + self.rows = rows; + } + + pub fn desired_height(&self, _width: u16) -> u16 { + let len = self.rows.len() as u16; + len.min(self.max_rows) + } +} + +impl WidgetRef for LiveRingWidget { + fn render_ref(&self, area: Rect, buf: &mut Buffer) { + if area.height == 0 { + return; + } + let visible = self.rows.len().saturating_sub(self.max_rows as usize); + let slice = &self.rows[visible..]; + let para = Paragraph::new(slice.to_vec()); + para.render_ref(area, buf); + } +} diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 7282650841..0c8610470c 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -9,6 +9,7 @@ use codex_file_search::FileMatch; use crossterm::event::KeyEvent; use ratatui::buffer::Buffer; use ratatui::layout::Rect; +use ratatui::text::Line; use ratatui::widgets::WidgetRef; mod approval_modal_view; @@ -17,6 +18,7 @@ mod chat_composer; mod chat_composer_history; mod command_popup; mod file_search_popup; +mod live_ring_widget; mod popup_consts; mod scroll_state; mod selection_popup_common; @@ -55,6 +57,10 @@ pub(crate) struct BottomPane<'a> { /// not replace the composer; it augments it. live_status: Option, + /// Optional transient ring shown above the composer. This is a rendering-only + /// container used during development before we wire it to ChatWidget events. + live_ring: Option, + /// True if the active view is the StatusIndicatorView that replaces the /// composer during a running task. status_view_active: bool, @@ -82,6 +88,7 @@ impl BottomPane<'_> { is_task_running: false, ctrl_c_quit_hint: false, live_status: None, + live_ring: None, status_view_active: false, } } @@ -92,14 +99,26 @@ impl BottomPane<'_> { .as_ref() .map(|s| s.desired_height(width)) .unwrap_or(0); + let ring_h = self + .live_ring + .as_ref() + .map(|r| r.desired_height(width)) + .unwrap_or(0); let view_height = if let Some(view) = self.active_view.as_ref() { - view.desired_height(width) + // Add a single blank spacer line between live ring and status view when active. + let spacer = if self.live_ring.is_some() && self.status_view_active { + 1 + } else { + 0 + }; + spacer + view.desired_height(width) } else { self.composer.desired_height(width) }; overlay_status_h + .saturating_add(ring_h) .saturating_add(view_height) .saturating_add(Self::BOTTOM_PAD_LINES) } @@ -333,11 +352,43 @@ impl BottomPane<'_> { self.composer.on_file_search_result(query, matches); self.request_redraw(); } + + /// Set the rows and cap for the transient live ring overlay. + pub(crate) fn set_live_ring_rows(&mut self, max_rows: u16, rows: Vec>) { + let mut w = live_ring_widget::LiveRingWidget::new(); + w.set_max_rows(max_rows); + w.set_rows(rows); + self.live_ring = Some(w); + } + + pub(crate) fn clear_live_ring(&mut self) { + self.live_ring = None; + } + + // Removed restart_live_status_with_text – no longer used by the current streaming UI. } impl WidgetRef for &BottomPane<'_> { fn render_ref(&self, area: Rect, buf: &mut Buffer) { let mut y_offset = 0u16; + if let Some(ring) = &self.live_ring { + let live_h = ring.desired_height(area.width).min(area.height); + if live_h > 0 { + let live_rect = Rect { + x: area.x, + y: area.y, + width: area.width, + height: live_h, + }; + ring.render_ref(live_rect, buf); + y_offset = live_h; + } + } + // Spacer between live ring and status view when active + if self.live_ring.is_some() && self.status_view_active && y_offset < area.height { + // Leave one empty line + y_offset = y_offset.saturating_add(1); + } if let Some(status) = &self.live_status { let live_h = status .desired_height(area.width) @@ -387,6 +438,7 @@ mod tests { use crate::app_event::AppEvent; use ratatui::buffer::Buffer; use ratatui::layout::Rect; + use ratatui::text::Line; use std::path::PathBuf; use std::sync::mpsc::channel; @@ -414,7 +466,103 @@ mod tests { assert_eq!(CancellationEvent::Ignored, pane.on_ctrl_c()); } - // live ring removed; related tests deleted. + #[test] + fn live_ring_renders_above_composer() { + let (tx_raw, _rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let mut pane = BottomPane::new(BottomPaneParams { + app_event_tx: tx, + has_input_focus: true, + enhanced_keys_supported: false, + }); + + // Provide 4 rows with max_rows=3; only the last 3 should be visible. + pane.set_live_ring_rows( + 3, + vec![ + Line::from("one".to_string()), + Line::from("two".to_string()), + Line::from("three".to_string()), + Line::from("four".to_string()), + ], + ); + + let area = Rect::new(0, 0, 10, 5); + let mut buf = Buffer::empty(area); + (&pane).render_ref(area, &mut buf); + + // Extract the first 3 rows and assert they contain the last three lines. + let mut lines: Vec = Vec::new(); + for y in 0..3 { + let mut s = String::new(); + for x in 0..area.width { + s.push(buf[(x, y)].symbol().chars().next().unwrap_or(' ')); + } + lines.push(s.trim_end().to_string()); + } + assert_eq!(lines, vec!["two", "three", "four"]); + } + + #[test] + fn status_indicator_visible_with_live_ring() { + let (tx_raw, _rx) = channel::(); + let tx = AppEventSender::new(tx_raw); + let mut pane = BottomPane::new(BottomPaneParams { + app_event_tx: tx, + has_input_focus: true, + enhanced_keys_supported: false, + }); + + // Simulate task running which replaces composer with the status indicator. + pane.set_task_running(true); + pane.update_status_text("waiting for model".to_string()); + + // Provide 2 rows in the live ring (e.g., streaming CoT) and ensure the + // status indicator remains visible below them. + pane.set_live_ring_rows( + 2, + vec![ + Line::from("cot1".to_string()), + Line::from("cot2".to_string()), + ], + ); + + // Allow some frames so the dot animation is present. + std::thread::sleep(std::time::Duration::from_millis(120)); + + // Height should include both ring rows, 1 spacer, and the 1-line status. + let area = Rect::new(0, 0, 30, 4); + let mut buf = Buffer::empty(area); + (&pane).render_ref(area, &mut buf); + + // Top two rows are the live ring. + let mut r0 = String::new(); + let mut r1 = String::new(); + for x in 0..area.width { + r0.push(buf[(x, 0)].symbol().chars().next().unwrap_or(' ')); + r1.push(buf[(x, 1)].symbol().chars().next().unwrap_or(' ')); + } + assert!(r0.contains("cot1"), "expected first live row: {r0:?}"); + assert!(r1.contains("cot2"), "expected second live row: {r1:?}"); + + // Row 2 is the spacer (blank) + let mut r2 = String::new(); + for x in 0..area.width { + r2.push(buf[(x, 2)].symbol().chars().next().unwrap_or(' ')); + } + assert!(r2.trim().is_empty(), "expected blank spacer line: {r2:?}"); + + // Bottom row is the status line; it should contain the left bar and "Working". + let mut r3 = String::new(); + for x in 0..area.width { + r3.push(buf[(x, 3)].symbol().chars().next().unwrap_or(' ')); + } + assert_eq!(buf[(0, 3)].symbol().chars().next().unwrap_or(' '), '▌'); + assert!( + r3.contains("Working"), + "expected Working header in status line: {r3:?}" + ); + } #[test] fn overlay_not_shown_above_approval_modal() { diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 075154016f..8a47353cbf 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::collections::VecDeque; use std::path::PathBuf; use std::sync::Arc; @@ -46,14 +45,13 @@ use crate::bottom_pane::BottomPane; use crate::bottom_pane::BottomPaneParams; use crate::bottom_pane::CancellationEvent; use crate::bottom_pane::InputResult; -use crate::exec_command::strip_bash_lc_and_escape; use crate::history_cell::CommandOutput; use crate::history_cell::HistoryCell; use crate::history_cell::PatchEventType; -use crate::markdown_stream::MarkdownNewlineCollector; -use crate::markdown_stream::RenderedLineStreamer; +use crate::live_wrap::RowBuilder; use crate::user_approval_widget::ApprovalRequest; use codex_file_search::FileMatch; +use ratatui::style::Stylize; struct RunningCommand { command: Vec, @@ -70,21 +68,17 @@ pub(crate) struct ChatWidget<'a> { initial_user_message: Option, total_token_usage: TokenUsage, last_token_usage: TokenUsage, - // Newline-gated markdown streaming state - reasoning_collector: MarkdownNewlineCollector, - answer_collector: MarkdownNewlineCollector, - reasoning_streamer: RenderedLineStreamer, - answer_streamer: RenderedLineStreamer, + reasoning_buffer: String, + content_buffer: String, + // Buffer for streaming assistant answer text; we do not surface partial + // We wait for the final AgentMessage event and then emit the full text + // at once into scrollback so the history contains a single message. + answer_buffer: String, running_commands: HashMap, + live_builder: RowBuilder, current_stream: Option, - // Track header emission per stream kind to avoid cross-stream duplication - answer_header_emitted: bool, - reasoning_header_emitted: bool, + stream_header_emitted: bool, live_max_rows: u16, - task_complete_pending: bool, - finishing_after_drain: bool, - // Queue of interruptive UI events deferred during an active write cycle - interrupt_queue: VecDeque, } struct UserMessage { @@ -98,15 +92,6 @@ enum StreamKind { Reasoning, } -#[derive(Debug)] -enum QueuedInterrupt { - ExecApproval(String, ExecApprovalRequestEvent), - ApplyPatchApproval(String, ApplyPatchApprovalRequestEvent), - ExecBegin(ExecCommandBeginEvent), - McpBegin(McpToolCallBeginEvent), - McpEnd(McpToolCallEndEvent), -} - impl From for UserMessage { fn from(text: String) -> Self { Self { @@ -125,173 +110,19 @@ fn create_initial_user_message(text: String, image_paths: Vec) -> Optio } impl ChatWidget<'_> { - fn header_line(kind: StreamKind) -> ratatui::text::Line<'static> { - use ratatui::style::Stylize; - match kind { - StreamKind::Reasoning => ratatui::text::Line::from("thinking".magenta().italic()), - StreamKind::Answer => ratatui::text::Line::from("codex".magenta().bold()), - } - } - fn line_is_blank(line: &ratatui::text::Line<'_>) -> bool { - if line.spans.is_empty() { - return true; - } - line.spans.iter().all(|s| s.content.trim().is_empty()) - } - /// Periodic tick to commit at most one queued line to history with a small delay, - /// animating the output. - pub(crate) fn on_commit_tick(&mut self) { - // Choose the active streamer - let (streamer, kind_opt) = match self.current_stream { - Some(StreamKind::Reasoning) => { - (&mut self.reasoning_streamer, Some(StreamKind::Reasoning)) - } - Some(StreamKind::Answer) => (&mut self.answer_streamer, Some(StreamKind::Answer)), - None => { - // No active stream. Nothing to animate. - return; - } - }; - - // Prepare header if needed - let mut lines: Vec> = Vec::new(); - if let Some(k) = kind_opt { - let header_needed = match k { - StreamKind::Reasoning => !self.reasoning_header_emitted, - StreamKind::Answer => !self.answer_header_emitted, - }; - if header_needed { - lines.push(Self::header_line(k)); - match k { - StreamKind::Reasoning => self.reasoning_header_emitted = true, - StreamKind::Answer => self.answer_header_emitted = true, - } - } - } - - let step = streamer.step(self.live_max_rows as usize); - if !step.history.is_empty() || !lines.is_empty() { - lines.extend(step.history); - self.app_event_tx.send(AppEvent::InsertHistory(lines)); - } - - // If streamer is now idle and there is no more active stream data, finalize state. - let is_idle = streamer.is_idle(); - if is_idle { - // Stop animation ticks between bursts. - self.app_event_tx.send(AppEvent::StopCommitAnimation); - if self.finishing_after_drain { - // Final cleanup once fully drained at end-of-stream. - self.current_stream = None; - self.finishing_after_drain = false; - if self.task_complete_pending { - self.bottom_pane.set_task_running(false); - self.task_complete_pending = false; - } - // After the write cycle completes, release any queued interrupts. - self.flush_interrupt_queue(); - } - } - } - fn is_write_cycle_active(&self) -> bool { - self.current_stream.is_some() - } - - fn flush_interrupt_queue(&mut self) { - while let Some(q) = self.interrupt_queue.pop_front() { - match q { - QueuedInterrupt::ExecApproval(id, ev) => self.handle_exec_approval_now(id, ev), - QueuedInterrupt::ApplyPatchApproval(id, ev) => { - self.handle_apply_patch_approval_now(id, ev) - } - QueuedInterrupt::ExecBegin(ev) => self.handle_exec_begin_now(ev), - QueuedInterrupt::McpBegin(ev) => self.handle_mcp_begin_now(ev), - QueuedInterrupt::McpEnd(ev) => self.handle_mcp_end_now(ev), - } - } - } - - fn handle_exec_approval_now(&mut self, id: String, ev: ExecApprovalRequestEvent) { - // Log a background summary immediately so the history is chronological. - let cmdline = strip_bash_lc_and_escape(&ev.command); - let text = format!( - "command requires approval:\n$ {cmdline}{reason}", - reason = ev - .reason - .as_ref() - .map(|r| format!("\n{r}")) - .unwrap_or_default() - ); - self.add_to_history(HistoryCell::new_background_event(text)); - - let request = ApprovalRequest::Exec { - id, - command: ev.command, - cwd: ev.cwd, - reason: ev.reason, - }; - self.bottom_pane.push_approval_request(request); - self.request_redraw(); - } - - fn handle_apply_patch_approval_now(&mut self, id: String, ev: ApplyPatchApprovalRequestEvent) { - self.add_to_history(HistoryCell::new_patch_event( - PatchEventType::ApprovalRequest, - ev.changes.clone(), - )); - - let request = ApprovalRequest::ApplyPatch { - id, - reason: ev.reason, - grant_root: ev.grant_root, - }; - self.bottom_pane.push_approval_request(request); - self.request_redraw(); - } - - fn handle_exec_begin_now(&mut self, ev: ExecCommandBeginEvent) { - // Ensure the status indicator is visible while the command runs. - self.bottom_pane - .update_status_text("running command".to_string()); - self.running_commands.insert( - ev.call_id.clone(), - RunningCommand { - command: ev.command.clone(), - cwd: ev.cwd.clone(), - }, - ); - self.active_history_cell = Some(HistoryCell::new_active_exec_command(ev.command)); - } - - fn handle_mcp_begin_now(&mut self, ev: McpToolCallBeginEvent) { - self.add_to_history(HistoryCell::new_active_mcp_tool_call(ev.invocation)); - } - - fn handle_mcp_end_now(&mut self, ev: McpToolCallEndEvent) { - self.add_to_history(HistoryCell::new_completed_mcp_tool_call( - 80, - ev.invocation, - ev.duration, - ev.result - .as_ref() - .map(|r| r.is_error.unwrap_or(false)) - .unwrap_or(false), - ev.result, - )); - } fn interrupt_running_task(&mut self) { if self.bottom_pane.is_task_running() { self.active_history_cell = None; self.bottom_pane.clear_ctrl_c_quit_hint(); self.submit_op(Op::Interrupt); self.bottom_pane.set_task_running(false); - self.reasoning_collector.clear(); - self.answer_collector.clear(); - self.reasoning_streamer.clear(); - self.answer_streamer.clear(); + self.bottom_pane.clear_live_ring(); + self.live_builder = RowBuilder::new(self.live_builder.width()); self.current_stream = None; - self.answer_header_emitted = false; - self.reasoning_header_emitted = false; + self.stream_header_emitted = false; + self.answer_buffer.clear(); + self.reasoning_buffer.clear(); + self.content_buffer.clear(); self.request_redraw(); } } @@ -306,7 +137,24 @@ impl ChatWidget<'_> { ]) .areas(area) } - + fn emit_stream_header(&mut self, kind: StreamKind) { + use ratatui::text::Line as RLine; + if self.stream_header_emitted { + return; + } + let header = match kind { + StreamKind::Reasoning => RLine::from("thinking".magenta().italic()), + StreamKind::Answer => RLine::from("codex".magenta().bold()), + }; + self.app_event_tx + .send(AppEvent::InsertHistory(vec![header])); + self.stream_header_emitted = true; + } + fn finalize_active_stream(&mut self) { + if let Some(kind) = self.current_stream { + self.finalize_stream(kind); + } + } pub(crate) fn new( config: Config, app_event_tx: AppEventSender, @@ -368,18 +216,14 @@ impl ChatWidget<'_> { ), total_token_usage: TokenUsage::default(), last_token_usage: TokenUsage::default(), - reasoning_collector: MarkdownNewlineCollector::new(), - answer_collector: MarkdownNewlineCollector::new(), - reasoning_streamer: RenderedLineStreamer::new(), - answer_streamer: RenderedLineStreamer::new(), + reasoning_buffer: String::new(), + content_buffer: String::new(), + answer_buffer: String::new(), running_commands: HashMap::new(), + live_builder: RowBuilder::new(80), current_stream: None, - answer_header_emitted: false, - reasoning_header_emitted: false, + stream_header_emitted: false, live_max_rows: 3, - task_complete_pending: false, - finishing_after_drain: false, - interrupt_queue: VecDeque::new(), } } @@ -476,6 +320,7 @@ impl ChatWidget<'_> { } EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta }) => { self.begin_stream(StreamKind::Answer); + self.answer_buffer.push_str(&delta); self.stream_push_and_maybe_commit(&delta); self.request_redraw(); } @@ -483,6 +328,7 @@ impl ChatWidget<'_> { // Stream CoT into the live pane; keep input visible and commit // overflow rows incrementally to scrollback. self.begin_stream(StreamKind::Reasoning); + self.reasoning_buffer.push_str(&delta); self.stream_push_and_maybe_commit(&delta); self.request_redraw(); } @@ -496,6 +342,7 @@ impl ChatWidget<'_> { }) => { // Treat raw reasoning content the same as summarized reasoning for UI flow. self.begin_stream(StreamKind::Reasoning); + self.reasoning_buffer.push_str(&delta); self.stream_push_and_maybe_commit(&delta); self.request_redraw(); } @@ -515,18 +362,9 @@ impl ChatWidget<'_> { EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message: _, }) => { - // Defer clearing status/live ring until streaming fully completes. - let streaming_active = match self.current_stream { - Some(StreamKind::Reasoning) => !self.reasoning_streamer.is_idle(), - Some(StreamKind::Answer) => !self.answer_streamer.is_idle(), - None => false, - }; - if streaming_active { - self.task_complete_pending = true; - } else { - self.bottom_pane.set_task_running(false); - self.request_redraw(); - } + self.bottom_pane.set_task_running(false); + self.bottom_pane.clear_live_ring(); + self.request_redraw(); } EventMsg::TokenCount(token_usage) => { self.total_token_usage = add_token_usage(&self.total_token_usage, &token_usage); @@ -540,42 +378,83 @@ impl ChatWidget<'_> { EventMsg::Error(ErrorEvent { message }) => { self.add_to_history(HistoryCell::new_error_event(message.clone())); self.bottom_pane.set_task_running(false); - self.reasoning_collector.clear(); - self.answer_collector.clear(); - self.reasoning_streamer.clear(); - self.answer_streamer.clear(); + self.bottom_pane.clear_live_ring(); + self.live_builder = RowBuilder::new(self.live_builder.width()); self.current_stream = None; - self.answer_header_emitted = false; - self.reasoning_header_emitted = false; + self.stream_header_emitted = false; + self.answer_buffer.clear(); + self.reasoning_buffer.clear(); + self.content_buffer.clear(); self.request_redraw(); } EventMsg::PlanUpdate(update) => { // Commit plan updates directly to history (no status-line preview). self.add_to_history(HistoryCell::new_plan_update(update)); } - EventMsg::ExecApprovalRequest(ev) => { - if self.is_write_cycle_active() { - self.interrupt_queue - .push_back(QueuedInterrupt::ExecApproval(id, ev)); - } else { - self.handle_exec_approval_now(id, ev); - } + EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { + call_id: _, + command, + cwd, + reason, + }) => { + self.finalize_active_stream(); + let request = ApprovalRequest::Exec { + id, + command, + cwd, + reason, + }; + self.bottom_pane.push_approval_request(request); + self.request_redraw(); } - EventMsg::ApplyPatchApprovalRequest(ev) => { - if self.is_write_cycle_active() { - self.interrupt_queue - .push_back(QueuedInterrupt::ApplyPatchApproval(id, ev)); - } else { - self.handle_apply_patch_approval_now(id, ev); - } + EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { + call_id: _, + changes, + reason, + grant_root, + }) => { + self.finalize_active_stream(); + // ------------------------------------------------------------------ + // Before we even prompt the user for approval we surface the patch + // summary in the main conversation so that the dialog appears in a + // sensible chronological order: + // (1) codex → proposes patch (HistoryCell::PendingPatch) + // (2) UI → asks for approval (BottomPane) + // This mirrors how command execution is shown (command begins → + // approval dialog) and avoids surprising the user with a modal + // prompt before they have seen *what* is being requested. + // ------------------------------------------------------------------ + self.add_to_history(HistoryCell::new_patch_event( + PatchEventType::ApprovalRequest, + changes, + )); + + // Now surface the approval request in the BottomPane as before. + let request = ApprovalRequest::ApplyPatch { + id, + reason, + grant_root, + }; + self.bottom_pane.push_approval_request(request); + self.request_redraw(); } - EventMsg::ExecCommandBegin(ev) => { - if self.is_write_cycle_active() { - self.interrupt_queue - .push_back(QueuedInterrupt::ExecBegin(ev)); - } else { - self.handle_exec_begin_now(ev); - } + EventMsg::ExecCommandBegin(ExecCommandBeginEvent { + call_id, + command, + cwd, + }) => { + self.finalize_active_stream(); + // Ensure the status indicator is visible while the command runs. + self.bottom_pane + .update_status_text("running command".to_string()); + self.running_commands.insert( + call_id, + RunningCommand { + command: command.clone(), + cwd: cwd.clone(), + }, + ); + self.active_history_cell = Some(HistoryCell::new_active_exec_command(command)); } EventMsg::ExecCommandOutputDelta(_) => { // TODO @@ -614,20 +493,29 @@ impl ChatWidget<'_> { }, )); } - EventMsg::McpToolCallBegin(ev) => { - if self.is_write_cycle_active() { - self.interrupt_queue - .push_back(QueuedInterrupt::McpBegin(ev)); - } else { - self.handle_mcp_begin_now(ev); - } + EventMsg::McpToolCallBegin(McpToolCallBeginEvent { + call_id: _, + invocation, + }) => { + self.finalize_active_stream(); + self.add_to_history(HistoryCell::new_active_mcp_tool_call(invocation)); } - EventMsg::McpToolCallEnd(ev) => { - if self.is_write_cycle_active() { - self.interrupt_queue.push_back(QueuedInterrupt::McpEnd(ev)); - } else { - self.handle_mcp_end_now(ev); - } + EventMsg::McpToolCallEnd(McpToolCallEndEvent { + call_id: _, + duration, + invocation, + result, + }) => { + self.add_to_history(HistoryCell::new_completed_mcp_tool_call( + 80, + invocation, + duration, + result + .as_ref() + .map(|r| r.is_error.unwrap_or(false)) + .unwrap_or(false), + result, + )); } EventMsg::GetHistoryEntryResponse(event) => { let codex_core::protocol::GetHistoryEntryResponseEvent { @@ -747,98 +635,62 @@ impl ChatWidget<'_> { } } -#[cfg(test)] -impl ChatWidget<'_> { - /// Test-only control to tune the maximum rows shown in the live overlay. - /// Useful for verifying queue-head behavior without changing production defaults. - pub fn test_set_live_max_rows(&mut self, n: u16) { - self.live_max_rows = n; - } -} - impl ChatWidget<'_> { fn begin_stream(&mut self, kind: StreamKind) { if let Some(current) = self.current_stream { if current != kind { - // Synchronously flush the previous stream to keep ordering sane. - let (collector, streamer) = match current { - StreamKind::Reasoning => { - (&mut self.reasoning_collector, &mut self.reasoning_streamer) - } - StreamKind::Answer => (&mut self.answer_collector, &mut self.answer_streamer), - }; - let remaining = collector.finalize_and_drain(&self.config); - if !remaining.is_empty() { - streamer.enqueue(remaining); - } - let step = streamer.drain_all(self.live_max_rows as usize); - let prev_header_emitted = match current { - StreamKind::Reasoning => self.reasoning_header_emitted, - StreamKind::Answer => self.answer_header_emitted, - }; - if !step.history.is_empty() || !prev_header_emitted { - let mut lines: Vec> = Vec::new(); - if !prev_header_emitted { - lines.push(Self::header_line(current)); - match current { - StreamKind::Reasoning => self.reasoning_header_emitted = true, - StreamKind::Answer => self.answer_header_emitted = true, - } - } - lines.extend(step.history); - // Ensure at most one blank separator after the flushed block. - if let Some(last) = lines.last() { - if !Self::line_is_blank(last) { - lines.push(ratatui::text::Line::from("")); - } - } else { - lines.push(ratatui::text::Line::from("")); - } - self.app_event_tx.send(AppEvent::InsertHistory(lines)); - } - // Reset for new stream - self.current_stream = None; + self.finalize_stream(current); } } if self.current_stream != Some(kind) { - // Only reset the header flag when switching FROM a different stream kind. - // If current_stream is None (e.g., transient idle), preserve header flags - // to avoid duplicate headers on re-entry into the same stream. - let prev = self.current_stream; self.current_stream = Some(kind); - if prev.is_some() { - match kind { - StreamKind::Reasoning => self.reasoning_header_emitted = false, - StreamKind::Answer => self.answer_header_emitted = false, - } - } + self.stream_header_emitted = false; + // Clear any previous live content; we're starting a new stream. + self.live_builder = RowBuilder::new(self.live_builder.width()); // Ensure the waiting status is visible (composer replaced). self.bottom_pane .update_status_text("waiting for model".to_string()); - // No live ring overlay; headers will be inserted with the first commit. + self.emit_stream_header(kind); } } fn stream_push_and_maybe_commit(&mut self, delta: &str) { - // Newline-gated: only consider committing when a newline is present. - let (collector, streamer) = match self.current_stream { - Some(StreamKind::Reasoning) => { - (&mut self.reasoning_collector, &mut self.reasoning_streamer) - } - Some(StreamKind::Answer) => (&mut self.answer_collector, &mut self.answer_streamer), - None => return, - }; + self.live_builder.push_fragment(delta); - collector.push_delta(delta); - if delta.contains('\n') { - let newly_completed = collector.commit_complete_lines(&self.config); - if !newly_completed.is_empty() { - streamer.enqueue(newly_completed); - // Start or continue commit animation. - self.app_event_tx.send(AppEvent::StartCommitAnimation); + // Commit overflow rows (small batches) while keeping the last N rows visible. + let drained = self + .live_builder + .drain_commit_ready(self.live_max_rows as usize); + if !drained.is_empty() { + let mut lines: Vec> = Vec::new(); + if !self.stream_header_emitted { + match self.current_stream { + Some(StreamKind::Reasoning) => { + lines.push(ratatui::text::Line::from("thinking".magenta().italic())); + } + Some(StreamKind::Answer) => { + lines.push(ratatui::text::Line::from("codex".magenta().bold())); + } + None => {} + } + self.stream_header_emitted = true; } + for r in drained { + lines.push(ratatui::text::Line::from(r.text)); + } + self.app_event_tx.send(AppEvent::InsertHistory(lines)); } + + // Update the live ring overlay lines (text-only, newest at bottom). + let rows = self + .live_builder + .display_rows() + .into_iter() + .map(|r| ratatui::text::Line::from(r.text)) + .collect::>(); + self.bottom_pane + .set_live_ring_rows(self.live_max_rows, rows); } fn finalize_stream(&mut self, kind: StreamKind) { @@ -846,21 +698,38 @@ impl ChatWidget<'_> { // Nothing to do; either already finalized or not the active stream. return; } - let (collector, streamer) = match kind { - StreamKind::Reasoning => (&mut self.reasoning_collector, &mut self.reasoning_streamer), - StreamKind::Answer => (&mut self.answer_collector, &mut self.answer_streamer), - }; - - let remaining = collector.finalize_and_drain(&self.config); - if !remaining.is_empty() { - streamer.enqueue(remaining); + // Flush any partial line as a full row, then drain all remaining rows. + self.live_builder.end_line(); + let remaining = self.live_builder.drain_rows(); + // TODO: Re-add markdown rendering for assistant answers and reasoning. + // When finalizing, pass the accumulated text through `markdown::append_markdown` + // to build styled `Line<'static>` entries instead of raw plain text lines. + if !remaining.is_empty() || !self.stream_header_emitted { + let mut lines: Vec> = Vec::new(); + if !self.stream_header_emitted { + match kind { + StreamKind::Reasoning => { + lines.push(ratatui::text::Line::from("thinking".magenta().italic())); + } + StreamKind::Answer => { + lines.push(ratatui::text::Line::from("codex".magenta().bold())); + } + } + self.stream_header_emitted = true; + } + for r in remaining { + lines.push(ratatui::text::Line::from(r.text)); + } + // Close the block with a blank line for readability. + lines.push(ratatui::text::Line::from("")); + self.app_event_tx.send(AppEvent::InsertHistory(lines)); } - // Trailing blank spacer - streamer.enqueue(vec![ratatui::text::Line::from("")]); - // Mark that we should clear state after draining. - self.finishing_after_drain = true; - // Start animation to drain remaining lines. Final cleanup will occur when drained. - self.app_event_tx.send(AppEvent::StartCommitAnimation); + + // Clear the live overlay and reset state for the next stream. + self.live_builder = RowBuilder::new(self.live_builder.width()); + self.bottom_pane.clear_live_ring(); + self.current_stream = None; + self.stream_header_emitted = false; } } @@ -901,34 +770,3 @@ fn add_token_usage(current_usage: &TokenUsage, new_usage: &TokenUsage) -> TokenU total_tokens: current_usage.total_tokens + new_usage.total_tokens, } } - -#[cfg(test)] -mod chatwidget_helper_tests { - use super::*; - use crate::app_event::AppEvent; - use crate::app_event_sender::AppEventSender; - use codex_core::config::ConfigOverrides; - use std::sync::mpsc::channel; - - fn test_config() -> Config { - let overrides = ConfigOverrides { - cwd: std::env::current_dir().ok(), - ..Default::default() - }; - match Config::load_with_cli_overrides(vec![], overrides) { - Ok(c) => c, - Err(e) => panic!("load test config: {e}"), - } - } - - #[tokio::test(flavor = "current_thread")] - async fn helpers_are_available_and_do_not_panic() { - let (tx_raw, _rx) = channel::(); - let tx = AppEventSender::new(tx_raw); - let cfg = test_config(); - let mut w = ChatWidget::new(cfg, tx, None, Vec::new(), false); - - // Adjust the live ring capacity (no-op for rendering) and ensure no panic. - w.test_set_live_max_rows(4); - } -} diff --git a/codex-rs/tui/src/chatwidget_stream_tests.rs b/codex-rs/tui/src/chatwidget_stream_tests.rs deleted file mode 100644 index 6757209017..0000000000 --- a/codex-rs/tui/src/chatwidget_stream_tests.rs +++ /dev/null @@ -1,392 +0,0 @@ -#[cfg(test)] -mod tests { - use std::sync::mpsc::{channel, Receiver}; - use std::time::Duration; - - use codex_core::config::Config; - use codex_core::config::ConfigOverrides; -use codex_core::protocol::{ - AgentMessageDeltaEvent, AgentMessageEvent, AgentReasoningDeltaEvent, AgentReasoningEvent, Event, EventMsg, -}; - - use crate::app_event::AppEvent; - use crate::app_event_sender::AppEventSender; - use crate::chatwidget::ChatWidget; - - fn test_config() -> Config { - let overrides = ConfigOverrides { - cwd: std::env::current_dir().ok(), - ..Default::default() - }; - match Config::load_with_cli_overrides(vec![], overrides) { - Ok(c) => c, - Err(e) => panic!("load test config: {e}"), - } - } - - fn recv_insert_history( - rx: &Receiver, - timeout_ms: u64, - ) -> Option>> { - let to = Duration::from_millis(timeout_ms); - match rx.recv_timeout(to) { - Ok(AppEvent::InsertHistory(lines)) => Some(lines), - Ok(_) => None, - Err(_) => None, - } - } - - #[test] - fn widget_streams_on_newline_and_header_once() { - let (tx_raw, rx) = channel::(); - let tx = AppEventSender::new(tx_raw); - let config = test_config(); - - let mut w = ChatWidget::new(config.clone(), tx.clone(), None, Vec::new(), false); - - // Start reasoning stream with partial content (no newline): expect no history yet. - w.handle_codex_event(Event { - id: "1".into(), - msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { - delta: "Hello".into(), - }), - }); - - // No history commit before newline. - assert!( - recv_insert_history(&rx, 50).is_none(), - "unexpected history before newline" - ); - - // No live overlay anymore; nothing visible until commit. - - // Push a newline which should cause commit of the first logical line. - w.handle_codex_event(Event { - id: "1".into(), - msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { - delta: " world\nNext".into(), - }), - }); - - let lines = match recv_insert_history(&rx, 200) { - Some(v) => v, - None => panic!("expected history after newline"), - }; - let rendered: Vec = lines - .iter() - .map(|l| { - l.spans - .iter() - .map(|s| s.content.clone()) - .collect::>() - .join("") - }) - .collect(); - - // First commit should include the header and the completed first line once. - assert!( - rendered.iter().any(|s| s.contains("thinking")), - "missing reasoning header: {rendered:?}" - ); - assert!( - rendered.iter().any(|s| s.contains("Hello world")), - "missing committed line: {rendered:?}" - ); - - // Send finalize; expect remaining content to flush and a trailing blank line. - w.handle_codex_event(Event { - id: "1".into(), - msg: EventMsg::AgentReasoning(AgentReasoningEvent { - text: String::new(), - }), - }); - - let lines2 = match recv_insert_history(&rx, 200) { - Some(v) => v, - None => panic!("expected history after finalize"), - }; - let rendered2: Vec = lines2 - .iter() - .map(|l| { - l.spans - .iter() - .map(|s| s.content.clone()) - .collect::>() - .join("") - }) - .collect(); - // Ensure header not repeated on finalize and a blank spacer exists at the end. - let header_count = rendered - .iter() - .chain(rendered2.iter()) - .filter(|s| s.contains("thinking")) - .count(); - assert_eq!(header_count, 1, "reasoning header should be emitted exactly once"); - assert!( - rendered2.last().is_some_and(|s| s.is_empty()), - "expected trailing blank line on finalize" - ); - } -} - -#[cfg(test)] -mod widget_stream_extra { - use super::*; - - #[test] - fn widget_fenced_code_slow_streaming_no_dup() { - let (tx_raw, rx) = channel::(); - let tx = AppEventSender::new(tx_raw); - let config = test_config(); - let mut w = ChatWidget::new(config.clone(), tx.clone(), None, Vec::new(), false); - - // Begin answer stream: push opening fence in pieces with no newline -> no history. - for d in ["```", ""] { - w.handle_codex_event(Event { - id: "a".into(), - msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: d.into() }), - }); - assert!(super::recv_insert_history(&rx, 30).is_none(), "no history before newline for fence"); - } - // Newline after fence line. - w.handle_codex_event(Event { - id: "a".into(), - msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: "\n".into() }), - }); - // This may or may not produce a visible line depending on renderer; accept either. - let _ = super::recv_insert_history(&rx, 100); - - // Stream the code line without newline -> no history. - w.handle_codex_event(Event { - id: "a".into(), - msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: "code line".into() }), - }); - assert!(super::recv_insert_history(&rx, 30).is_none(), "no history before newline for code line"); - - // Now newline to commit the code line. - w.handle_codex_event(Event { - id: "a".into(), - msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: "\n".into() }), - }); - let commit1 = match super::recv_insert_history(&rx, 200) { - Some(v) => v, - None => panic!("history after code line newline"), - }; - - // Close fence slowly then newline. - w.handle_codex_event(Event { - id: "a".into(), - msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: "```".into() }), - }); - assert!(super::recv_insert_history(&rx, 30).is_none(), "no history before closing fence newline"); - w.handle_codex_event(Event { - id: "a".into(), - msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: "\n".into() }), - }); - let _ = super::recv_insert_history(&rx, 100); - - // Finalize should not duplicate the code line and should add a trailing blank. - w.handle_codex_event(Event { - id: "a".into(), - msg: EventMsg::AgentMessage(AgentMessageEvent { message: String::new() }), - }); - let commit2 = match super::recv_insert_history(&rx, 200) { - Some(v) => v, - None => panic!("history after finalize"), - }; - - let texts1: Vec = commit1 - .iter() - .map(|l| l.spans.iter().map(|s| s.content.clone()).collect::()) - .collect(); - let texts2: Vec = commit2 - .iter() - .map(|l| l.spans.iter().map(|s| s.content.clone()).collect::()) - .collect(); - let all = [texts1, texts2].concat(); - let code_count = all.iter().filter(|s| s.contains("code line")).count(); - assert_eq!(code_count, 1, "code line should appear exactly once in history: {all:?}"); - assert!(all.iter().all(|s| !s.contains("```")), "backticks should not be shown in history: {all:?}"); - } - - #[test] - fn widget_rendered_trickle_live_ring_head() { - let (tx_raw, rx) = channel::(); - let tx = AppEventSender::new(tx_raw); - let config = test_config(); - let mut w = ChatWidget::new(config.clone(), tx.clone(), None, Vec::new(), false); - - // Increase live ring capacity so it can include queue head. - w.test_set_live_max_rows(4); - - // Enqueue 5 completed lines in a single delta. - let payload = "l1\nl2\nl3\nl4\nl5\n".to_string(); - w.handle_codex_event(Event { - id: "b".into(), - msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: payload }), - }); - - // First batch commit: expect header + 3 lines. - let lines = match super::recv_insert_history(&rx, 200) { - Some(v) => v, - None => panic!("history after batch"), - }; - let rendered: Vec = lines - .iter() - .map(|l| l.spans.iter().map(|s| s.content.clone()).collect::()) - .collect(); - assert!(rendered.iter().any(|s| s.contains("codex")), "answer header missing"); - let committed: Vec<_> = rendered.into_iter().filter(|s| s.starts_with('l')).collect(); - assert_eq!(committed.len(), 3, "expected 3 committed lines in first batch"); - - // No live overlay anymore; only committed lines appear in history. - - // Finalize: drain the remaining lines. - w.handle_codex_event(Event { - id: "b".into(), - msg: EventMsg::AgentMessage(AgentMessageEvent { message: String::new() }), - }); - let lines2 = match super::recv_insert_history(&rx, 200) { - Some(v) => v, - None => panic!("history after finalize"), - }; - let rendered2: Vec = lines2 - .iter() - .map(|l| l.spans.iter().map(|s| s.content.clone()).collect::()) - .collect(); - assert!(rendered2.iter().any(|s| s == "l4")); - assert!(rendered2.iter().any(|s| s == "l5")); - assert!(rendered2.last().is_some_and(|s| s.is_empty()), "expected trailing blank line after finalize"); - } - - #[test] - fn widget_reasoning_then_answer_ordering() { - let (tx_raw, rx) = channel::(); - let tx = AppEventSender::new(tx_raw); - let config = test_config(); - let mut w = ChatWidget::new(config.clone(), tx.clone(), None, Vec::new(), false); - - // Reasoning: one completed line then finalize. - w.handle_codex_event(Event { - id: "ra".into(), - msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta: "think1\n".into() }), - }); - let r_commit = match super::recv_insert_history(&rx, 200) { - Some(v) => v, - None => panic!("reasoning history"), - }; - w.handle_codex_event(Event { - id: "ra".into(), - msg: EventMsg::AgentReasoning(AgentReasoningEvent { text: String::new() }), - }); - let r_final = match super::recv_insert_history(&rx, 200) { - Some(v) => v, - None => panic!("reasoning finalize"), - }; - - // Answer: one completed line then finalize. - w.handle_codex_event(Event { - id: "ra".into(), - msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta: "ans1\n".into() }), - }); - let a_commit = match super::recv_insert_history(&rx, 200) { - Some(v) => v, - None => panic!("answer history"), - }; - w.handle_codex_event(Event { - id: "ra".into(), - msg: EventMsg::AgentMessage(AgentMessageEvent { message: String::new() }), - }); - let a_final = match super::recv_insert_history(&rx, 200) { - Some(v) => v, - None => panic!("answer finalize"), - }; - - let to_texts = |lines: &Vec>| -> Vec { - lines - .iter() - .map(|l| l.spans.iter().map(|s| s.content.clone()).collect::()) - .collect() - }; - let r_all = [to_texts(&r_commit), to_texts(&r_final)].concat(); - let a_all = [to_texts(&a_commit), to_texts(&a_final)].concat(); - - // Expect headers present and in order: reasoning first, then answer. - let r_header_idx = match r_all.iter().position(|s| s.contains("thinking")) { - Some(i) => i, - None => panic!("missing reasoning header"), - }; - let a_header_idx = match a_all.iter().position(|s| s.contains("codex")) { - Some(i) => i, - None => panic!("missing answer header"), - }; - assert!(r_all.iter().any(|s| s == "think1"), "missing reasoning content: {:?}", r_all); - assert!(a_all.iter().any(|s| s == "ans1"), "missing answer content: {:?}", a_all); - // Implicitly, reasoning events happened before answer events if we got here without timeouts. - assert_eq!(r_header_idx, 0, "reasoning header should be first in its batch"); - assert_eq!(a_header_idx, 0, "answer header should be first in its batch"); - } - - #[test] - fn header_not_repeated_across_pauses() { - let (tx_raw, rx) = channel::(); - let tx = AppEventSender::new(tx_raw); - let config = test_config(); - let mut w = ChatWidget::new(config.clone(), tx.clone(), None, Vec::new(), false); - - // Begin reasoning, enqueue first line, start animation. - w.handle_codex_event(Event { - id: "r1".into(), - msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta: "first\n".into() }), - }); - // Simulate one animation tick: should emit header + first. - w.on_commit_tick(); - let lines1 = super::recv_insert_history(&rx, 200).expect("history after first tick"); - let texts1: Vec = lines1 - .iter() - .map(|l| l.spans.iter().map(|s| s.content.clone()).collect::()) - .collect(); - assert!(texts1.iter().any(|s| s.contains("thinking")), "missing header on first tick: {texts1:?}"); - assert!(texts1.iter().any(|s| s == "first"), "missing first line: {texts1:?}"); - - // Stop ticks naturally by draining queue (second tick consumes nothing). - w.on_commit_tick(); - let _ = super::recv_insert_history(&rx, 100); - - // Later, enqueue another completed line; header must NOT repeat. - w.handle_codex_event(Event { - id: "r1".into(), - msg: EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta: "second\n".into() }), - }); - w.on_commit_tick(); - let lines2 = super::recv_insert_history(&rx, 200).expect("history after second tick"); - let texts2: Vec = lines2 - .iter() - .map(|l| l.spans.iter().map(|s| s.content.clone()).collect::()) - .collect(); - let header_count2 = texts2.iter().filter(|s| s.contains("thinking")).count(); - assert_eq!(header_count2, 0, "header should not repeat after pause: {texts2:?}"); - assert!(texts2.iter().any(|s| s == "second"), "missing second line: {texts2:?}"); - - // Finalize; trailing blank should be added; no extra header. - w.handle_codex_event(Event { - id: "r1".into(), - msg: EventMsg::AgentReasoning(AgentReasoningEvent { text: String::new() }), - }); - // Drain remaining with ticks. - w.on_commit_tick(); - let lines3 = super::recv_insert_history(&rx, 200).expect("history after finalize tick"); - let texts3: Vec = lines3 - .iter() - .map(|l| l.spans.iter().map(|s| s.content.clone()).collect::()) - .collect(); - let header_total = texts1 - .into_iter() - .chain(texts2.into_iter()) - .chain(texts3.iter().cloned()) - .filter(|s| s.contains("thinking")) - .count(); - assert_eq!(header_total, 1, "header should appear exactly once across pauses and finalize"); - assert!(texts3.last().is_some_and(|s| s.is_empty()), "expected trailing blank line"); - } -} diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 3cbd39c1f1..443c54aa9b 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -1,6 +1,5 @@ use crate::exec_command::relativize_to_home; use crate::exec_command::strip_bash_lc_and_escape; -use crate::insert_history::word_wrap_lines; use crate::slash_command::SlashCommand; use crate::text_block::TextBlock; use crate::text_formatting::format_and_truncate_tool_result; @@ -31,6 +30,7 @@ use ratatui::text::Line as RtLine; use ratatui::text::Span as RtSpan; use ratatui::widgets::Paragraph; use ratatui::widgets::WidgetRef; +use ratatui::widgets::Wrap; use std::collections::HashMap; use std::io::Cursor; use std::path::PathBuf; @@ -187,8 +187,11 @@ impl HistoryCell { } pub(crate) fn desired_height(&self, width: u16) -> u16 { - let wrapped = word_wrap_lines(&self.plain_lines(), width); - wrapped.len() as u16 + Paragraph::new(Text::from(self.plain_lines())) + .wrap(Wrap { trim: false }) + .line_count(width) + .try_into() + .unwrap_or(0) } pub(crate) fn new_session_info( @@ -818,8 +821,9 @@ impl HistoryCell { impl WidgetRef for &HistoryCell { fn render_ref(&self, area: Rect, buf: &mut Buffer) { - let wrapped = word_wrap_lines(&self.plain_lines(), area.width); - Paragraph::new(Text::from(wrapped)).render(area, buf); + Paragraph::new(Text::from(self.plain_lines())) + .wrap(Wrap { trim: false }) + .render(area, buf); } } diff --git a/codex-rs/tui/src/insert_history.rs b/codex-rs/tui/src/insert_history.rs index 971c376234..5c316637b1 100644 --- a/codex-rs/tui/src/insert_history.rs +++ b/codex-rs/tui/src/insert_history.rs @@ -18,8 +18,6 @@ use ratatui::style::Color; use ratatui::style::Modifier; use ratatui::text::Line; use ratatui::text::Span; -use textwrap::Options as TwOptions; -use textwrap::WordSplitter; /// Insert `lines` above the viewport. pub(crate) fn insert_history_lines(terminal: &mut tui::Tui, lines: Vec) { @@ -42,10 +40,7 @@ pub fn insert_history_lines_to_writer( let mut area = terminal.get_frame().area(); - // Pre-wrap lines using word-aware wrapping so terminal scrollback sees the same - // formatting as the TUI. This avoids character-level hard wrapping by the terminal. - let wrapped = word_wrap_lines(&lines, area.width.max(1)); - let wrapped_lines = wrapped.len() as u16; + let wrapped_lines = wrapped_line_count(&lines, area.width); let cursor_top = if area.bottom() < screen_size.height { // If the viewport is not at the bottom of the screen, scroll it down to make room. // Don't scroll it past the bottom of the screen. @@ -96,7 +91,7 @@ pub fn insert_history_lines_to_writer( // fetch/restore the cursor position. insert_history_lines should be cursor-position-neutral :) queue!(writer, MoveTo(0, cursor_top)).ok(); - for line in wrapped { + for line in lines { queue!(writer, Print("\r\n")).ok(); write_spans(writer, line.iter()).ok(); } @@ -109,6 +104,36 @@ pub fn insert_history_lines_to_writer( } } +fn wrapped_line_count(lines: &[Line], width: u16) -> u16 { + let mut count = 0; + for line in lines { + count += line_height(line, width); + } + count +} + +fn line_height(line: &Line, width: u16) -> u16 { + // Use the same visible-width slicing semantics as the live row builder so + // our pre-scroll estimation matches how rows will actually wrap. + let w = width.max(1) as usize; + let mut rows = 0u16; + let mut remaining = line + .spans + .iter() + .map(|s| s.content.as_ref()) + .collect::>() + .join(""); + while !remaining.is_empty() { + let (_prefix, suffix, taken) = crate::live_wrap::take_prefix_by_width(&remaining, w); + rows = rows.saturating_add(1); + if taken >= remaining.len() { + break; + } + remaining = suffix.to_string(); + } + rows.max(1) +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct SetScrollRegion(pub std::ops::Range); @@ -257,126 +282,6 @@ where ) } -/// Word-aware wrapping for a list of `Line`s preserving styles. -pub(crate) fn word_wrap_lines(lines: &[Line], width: u16) -> Vec> { - let mut out = Vec::new(); - let w = width.max(1) as usize; - for line in lines { - out.extend(word_wrap_line(line, w)); - } - out -} - -fn word_wrap_line(line: &Line, width: usize) -> Vec> { - if width == 0 { - return vec![to_owned_line(line)]; - } - // Concatenate content and keep span boundaries for later re-slicing. - let mut flat = String::new(); - let mut span_bounds = Vec::new(); // (start_byte, end_byte, style) - let mut cursor = 0usize; - for s in &line.spans { - let text = s.content.as_ref(); - let start = cursor; - flat.push_str(text); - cursor += text.len(); - span_bounds.push((start, cursor, s.style)); - } - - // Use textwrap for robust word-aware wrapping; no hyphenation, no breaking words. - let opts = TwOptions::new(width) - .break_words(false) - .word_splitter(WordSplitter::NoHyphenation); - let wrapped = textwrap::wrap(&flat, &opts); - - if wrapped.len() <= 1 { - return vec![to_owned_line(line)]; - } - - // Map wrapped pieces back to byte ranges in `flat` sequentially. - let mut start_cursor = 0usize; - let mut out: Vec> = Vec::with_capacity(wrapped.len()); - for piece in wrapped { - let piece_str: &str = &piece; - if piece_str.is_empty() { - out.push(Line { - style: line.style, - alignment: line.alignment, - spans: Vec::new(), - }); - continue; - } - // Find the next occurrence of piece_str at or after start_cursor. - // textwrap preserves order, so a linear scan is sufficient. - if let Some(rel) = flat[start_cursor..].find(piece_str) { - let s = start_cursor + rel; - let e = s + piece_str.len(); - out.push(slice_line_spans(line, &span_bounds, s, e)); - start_cursor = e; - } else { - // Fallback: slice by length from cursor. - let s = start_cursor; - let e = (start_cursor + piece_str.len()).min(flat.len()); - out.push(slice_line_spans(line, &span_bounds, s, e)); - start_cursor = e; - } - } - - out -} - -fn to_owned_line(l: &Line<'_>) -> Line<'static> { - Line { - style: l.style, - alignment: l.alignment, - spans: l - .spans - .iter() - .map(|s| Span { - style: s.style, - content: std::borrow::Cow::Owned(s.content.to_string()), - }) - .collect(), - } -} - -fn slice_line_spans( - original: &Line<'_>, - span_bounds: &[(usize, usize, ratatui::style::Style)], - start_byte: usize, - end_byte: usize, -) -> Line<'static> { - let mut acc: Vec> = Vec::new(); - for (i, (s, e, style)) in span_bounds.iter().enumerate() { - if *e <= start_byte { - continue; - } - if *s >= end_byte { - break; - } - let seg_start = start_byte.max(*s); - let seg_end = end_byte.min(*e); - if seg_end > seg_start { - let local_start = seg_start - *s; - let local_end = seg_end - *s; - let content = original.spans[i].content.as_ref(); - let slice = &content[local_start..local_end]; - acc.push(Span { - style: *style, - content: std::borrow::Cow::Owned(slice.to_string()), - }); - } - if *e >= end_byte { - break; - } - } - Line { - style: original.style, - alignment: original.alignment, - spans: acc, - } -} - #[cfg(test)] mod tests { #![allow(clippy::unwrap_used)] @@ -413,34 +318,8 @@ mod tests { #[test] fn line_height_counts_double_width_emoji() { let line = Line::from("😀😀😀"); // each emoji ~ width 2 - assert_eq!(word_wrap_line(&line, 4).len(), 2); - assert_eq!(word_wrap_line(&line, 2).len(), 3); - assert_eq!(word_wrap_line(&line, 6).len(), 1); - } - - #[test] - fn word_wrap_does_not_split_words_simple_english() { - let sample = "Years passed, and Willowmere thrived in peace and friendship. Mira’s herb garden flourished with both ordinary and enchanted plants, and travelers spoke of the kindness of the woman who tended them."; - let line = Line::from(sample); - // Force small width to exercise wrapping at spaces. - let wrapped = word_wrap_lines(&[line], 40); - let joined: String = wrapped - .iter() - .map(|l| { - l.spans - .iter() - .map(|s| s.content.clone()) - .collect::() - }) - .collect::>() - .join("\n"); - assert!( - !joined.contains("bo\nth"), - "word 'both' should not be split across lines:\n{joined}" - ); - assert!( - !joined.contains("Willowm\nere"), - "should not split inside words:\n{joined}" - ); + assert_eq!(line_height(&line, 4), 2); + assert_eq!(line_height(&line, 2), 3); + assert_eq!(line_height(&line, 6), 1); } } diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 056ece9feb..e15a235a71 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -39,7 +39,6 @@ pub mod insert_history; pub mod live_wrap; mod log_layer; mod markdown; -mod markdown_stream; pub mod onboarding; mod shimmer; mod slash_command; @@ -56,8 +55,6 @@ use color_eyre::owo_colors::OwoColorize; pub use cli::Cli; -// (tests access modules directly within the crate) - pub async fn run_main( cli: Cli, codex_linux_sandbox_exe: Option, diff --git a/codex-rs/tui/src/markdown.rs b/codex-rs/tui/src/markdown.rs index 124c7c06b2..910a6869ec 100644 --- a/codex-rs/tui/src/markdown.rs +++ b/codex-rs/tui/src/markdown.rs @@ -22,35 +22,35 @@ fn append_markdown_with_opener_and_cwd( file_opener: UriBasedFileOpener, cwd: &Path, ) { - // Historically, we fed the entire `markdown_source` into the renderer in - // one pass. However, fenced code blocks sometimes lost leading whitespace - // when formatted by the markdown renderer/highlighter. To preserve code - // block content exactly, split the source into "text" and "code" segments: - // - Render non-code text through `tui_markdown` (with citation rewrite). - // - Render code block content verbatim as plain lines without additional - // formatting, preserving leading spaces. - for seg in split_text_and_fences(markdown_source) { - match seg { - Segment::Text(s) => { - let processed = rewrite_file_citations(&s, file_opener, cwd); - let rendered = tui_markdown::from_str(&processed); - push_owned_lines(rendered.lines, lines); - } - Segment::Code { content, .. } => { - // Emit the code content exactly as-is, line by line. - // We don't attempt syntax highlighting to avoid whitespace bugs. - for line in content.split_inclusive('\n') { - // split_inclusive keeps the trailing \n; we want lines without it. - let line = if let Some(stripped) = line.strip_suffix('\n') { - stripped - } else { - line - }; - let owned_line: Line<'static> = Line::from(Span::raw(line.to_string())); - lines.push(owned_line); - } - } + // Perform citation rewrite *before* feeding the string to the markdown + // renderer. When `file_opener` is absent we bypass the transformation to + // avoid unnecessary allocations. + let processed_markdown = rewrite_file_citations(markdown_source, file_opener, cwd); + + let markdown = tui_markdown::from_str(&processed_markdown); + + // `tui_markdown` returns a `ratatui::text::Text` where every `Line` borrows + // from the input `message` string. Since the `HistoryCell` stores its lines + // with a `'static` lifetime we must create an **owned** copy of each line + // so that it is no longer tied to `message`. We do this by cloning the + // content of every `Span` into an owned `String`. + + for borrowed_line in markdown.lines { + let mut owned_spans = Vec::with_capacity(borrowed_line.spans.len()); + for span in &borrowed_line.spans { + // Create a new owned String for the span's content to break the lifetime link. + let owned_span = Span::styled(span.content.to_string(), span.style); + owned_spans.push(owned_span); } + + let owned_line: Line<'static> = Line::from(owned_spans).style(borrowed_line.style); + // Preserve alignment if it was set on the source line. + let owned_line = match borrowed_line.alignment { + Some(alignment) => owned_line.alignment(alignment), + None => owned_line, + }; + + lines.push(owned_line); } } @@ -101,177 +101,6 @@ fn rewrite_file_citations<'a>( }) } -// Helper to clone borrowed ratatui lines into owned lines with 'static lifetime. -fn push_owned_lines<'a>(borrowed: Vec>, out: &mut Vec>) { - for borrowed_line in borrowed { - let mut owned_spans = Vec::with_capacity(borrowed_line.spans.len()); - for span in &borrowed_line.spans { - let owned_span = Span::styled(span.content.to_string(), span.style); - owned_spans.push(owned_span); - } - let owned_line: Line<'static> = Line::from(owned_spans).style(borrowed_line.style); - let owned_line = match borrowed_line.alignment { - Some(alignment) => owned_line.alignment(alignment), - None => owned_line, - }; - out.push(owned_line); - } -} - -// Minimal code block splitting. -// - Recognizes fenced blocks opened by ``` or ~~~ (allowing leading whitespace). -// The opening fence may include a language string which we ignore. -// The closing fence must be on its own line (ignoring surrounding whitespace). -// - Additionally recognizes indented code blocks that begin after a blank line -// with a line starting with at least 4 spaces or a tab, and continue for -// consecutive lines that are blank or also indented by >= 4 spaces or a tab. -enum Segment { - Text(String), - Code { - _lang: Option, - content: String, - }, -} - -fn split_text_and_fences(src: &str) -> Vec { - let mut segments = Vec::new(); - let mut curr_text = String::new(); - #[derive(Copy, Clone, PartialEq)] - enum CodeMode { - None, - Fenced, - Indented, - } - let mut code_mode = CodeMode::None; - let mut fence_token = ""; - let mut code_lang: Option = None; - let mut code_content = String::new(); - // We intentionally do not require a preceding blank line for indented code blocks, - // since streamed model output often omits it. This favors preserving indentation. - - for line in src.split_inclusive('\n') { - let line_no_nl = line.strip_suffix('\n'); - let trimmed_start = match line_no_nl { - Some(l) => l.trim_start(), - None => line.trim_start(), - }; - if code_mode == CodeMode::None { - let open = if trimmed_start.starts_with("```") { - Some("```") - } else if trimmed_start.starts_with("~~~") { - Some("~~~") - } else { - None - }; - if let Some(tok) = open { - // Flush pending text segment. - if !curr_text.is_empty() { - segments.push(Segment::Text(curr_text.clone())); - curr_text.clear(); - } - fence_token = tok; - // Capture language after the token on this line (before newline). - let after = &trimmed_start[tok.len()..]; - let lang = after.trim(); - code_lang = if lang.is_empty() { - None - } else { - Some(lang.to_string()) - }; - code_mode = CodeMode::Fenced; - code_content.clear(); - // Do not include the opening fence line in output. - continue; - } - // Check for start of an indented code block: only after a blank line - // (or at the beginning), and the line must start with >=4 spaces or a tab. - let raw_line = match line_no_nl { - Some(l) => l, - None => line, - }; - let leading_spaces = raw_line.chars().take_while(|c| *c == ' ').count(); - let starts_with_tab = raw_line.starts_with('\t'); - // Consider any line that begins with >=4 spaces or a tab to start an - // indented code block. This favors preserving indentation even when a - // preceding blank line is omitted (common in streamed model output). - let starts_indented_code = (leading_spaces >= 4) || starts_with_tab; - if starts_indented_code { - // Flush pending text and begin an indented code block. - if !curr_text.is_empty() { - segments.push(Segment::Text(curr_text.clone())); - curr_text.clear(); - } - code_mode = CodeMode::Indented; - code_content.clear(); - code_content.push_str(line); - // Inside code now; do not treat this line as normal text. - continue; - } - // Normal text line. - curr_text.push_str(line); - } else { - match code_mode { - CodeMode::Fenced => { - // inside fenced code: check for closing fence on its own line - let trimmed = match line_no_nl { - Some(l) => l.trim(), - None => line.trim(), - }; - if trimmed == fence_token { - // End code block: emit segment without fences - segments.push(Segment::Code { - _lang: code_lang.take(), - content: code_content.clone(), - }); - code_content.clear(); - code_mode = CodeMode::None; - fence_token = ""; - continue; - } - // Accumulate code content exactly as-is. - code_content.push_str(line); - } - CodeMode::Indented => { - // Continue while the line is blank, or starts with >=4 spaces, or a tab. - let raw_line = match line_no_nl { - Some(l) => l, - None => line, - }; - let is_blank = raw_line.trim().is_empty(); - let leading_spaces = raw_line.chars().take_while(|c| *c == ' ').count(); - let starts_with_tab = raw_line.starts_with('\t'); - if is_blank || leading_spaces >= 4 || starts_with_tab { - code_content.push_str(line); - } else { - // Close the indented code block and reprocess this line as normal text. - segments.push(Segment::Code { - _lang: None, - content: code_content.clone(), - }); - code_content.clear(); - code_mode = CodeMode::None; - // Now handle current line as text. - curr_text.push_str(line); - } - } - CodeMode::None => unreachable!(), - } - } - } - - if code_mode != CodeMode::None { - // Unterminated code fence: treat accumulated content as a code segment. - segments.push(Segment::Code { - _lang: code_lang.take(), - content: code_content.clone(), - }); - } else if !curr_text.is_empty() { - segments.push(Segment::Text(curr_text.clone())); - } - - segments -} - #[cfg(test)] mod tests { use super::*; @@ -333,99 +162,4 @@ mod tests { // Ensure helper rewrites. assert_ne!(markdown, unchanged); } - - #[test] - fn fenced_code_blocks_preserve_leading_whitespace() { - let src = "```\n indented\n\t\twith tabs\n four spaces\n```\n"; - let cwd = Path::new("/"); - let mut out = Vec::new(); - append_markdown_with_opener_and_cwd(src, &mut out, UriBasedFileOpener::None, cwd); - let rendered: Vec = out - .iter() - .map(|l| { - l.spans - .iter() - .map(|s| s.content.clone()) - .collect::() - }) - .collect(); - assert_eq!( - rendered, - vec![ - " indented".to_string(), - "\t\twith tabs".to_string(), - " four spaces".to_string() - ] - ); - } - - #[test] - fn citations_not_rewritten_inside_code_blocks() { - let src = "Before 【F:/x.rs†L1】\n```\nInside 【F:/x.rs†L2】\n```\nAfter 【F:/x.rs†L3】\n"; - let cwd = Path::new("/"); - let mut out = Vec::new(); - append_markdown_with_opener_and_cwd(src, &mut out, UriBasedFileOpener::VsCode, cwd); - let rendered: Vec = out - .iter() - .map(|l| { - l.spans - .iter() - .map(|s| s.content.clone()) - .collect::() - }) - .collect(); - // Expect first and last lines rewritten, middle line unchanged. - assert!(rendered[0].contains("vscode://file")); - assert_eq!(rendered[1], "Inside 【F:/x.rs†L2】"); - assert!(matches!(rendered.last(), Some(s) if s.contains("vscode://file"))); - } - - #[test] - fn indented_code_blocks_preserve_leading_whitespace() { - let src = "Before\n code 1\n\tcode with tab\n code 2\nAfter\n"; - let cwd = Path::new("/"); - let mut out = Vec::new(); - append_markdown_with_opener_and_cwd(src, &mut out, UriBasedFileOpener::None, cwd); - let rendered: Vec = out - .iter() - .map(|l| { - l.spans - .iter() - .map(|s| s.content.clone()) - .collect::() - }) - .collect(); - assert_eq!( - rendered, - vec![ - "Before".to_string(), - " code 1".to_string(), - "\tcode with tab".to_string(), - " code 2".to_string(), - "After".to_string() - ] - ); - } - - #[test] - fn citations_not_rewritten_inside_indented_code_blocks() { - let src = "Start 【F:/x.rs†L1】\n\n Inside 【F:/x.rs†L2】\n\nEnd 【F:/x.rs†L3】\n"; - let cwd = Path::new("/"); - let mut out = Vec::new(); - append_markdown_with_opener_and_cwd(src, &mut out, UriBasedFileOpener::VsCode, cwd); - let rendered: Vec = out - .iter() - .map(|l| { - l.spans - .iter() - .map(|s| s.content.clone()) - .collect::() - }) - .collect(); - // Expect first and last lines rewritten, and the indented code line present - // unchanged (citations inside not rewritten). We do not assert on blank - // separator lines since the markdown renderer may normalize them. - assert!(rendered.iter().any(|s| s.contains("vscode://file"))); - assert!(rendered.iter().any(|s| s == " Inside 【F:/x.rs†L2】")); - } } diff --git a/codex-rs/tui/src/markdown_stream.rs b/codex-rs/tui/src/markdown_stream.rs deleted file mode 100644 index 9eeb1740a1..0000000000 --- a/codex-rs/tui/src/markdown_stream.rs +++ /dev/null @@ -1,565 +0,0 @@ -use std::collections::VecDeque; - -use codex_core::config::Config; -use ratatui::text::Line; - -use crate::markdown; - -/// Newline-gated accumulator that renders markdown and commits only fully -/// completed logical lines. -pub(crate) struct MarkdownNewlineCollector { - buffer: String, - committed_line_count: usize, -} - -impl MarkdownNewlineCollector { - pub fn new() -> Self { - Self { - buffer: String::new(), - committed_line_count: 0, - } - } - - pub fn clear(&mut self) { - self.buffer.clear(); - self.committed_line_count = 0; - } - - pub fn push_delta(&mut self, delta: &str) { - self.buffer.push_str(delta); - } - - /// Render the full buffer and return only the newly completed logical lines - /// since the last commit. When the buffer does not end with a newline, the - /// final rendered line is considered incomplete and is not emitted. - pub fn commit_complete_lines(&mut self, config: &Config) -> Vec> { - // In non-test builds, unwrap an outer ```markdown fence during commit as well, - // so fence markers never appear in streamed history. - let source = unwrap_markdown_language_fence_if_enabled(self.buffer.clone()); - let source = strip_empty_fenced_code_blocks(&source); - - let mut rendered: Vec> = Vec::new(); - markdown::append_markdown(&source, &mut rendered, config); - - let mut complete_line_count = rendered.len(); - if complete_line_count > 0 && is_effectively_empty(&rendered[complete_line_count - 1]) { - complete_line_count -= 1; - } - if !self.buffer.ends_with('\n') { - complete_line_count = complete_line_count.saturating_sub(1); - // If we're inside an unclosed fenced code block, also drop the - // last rendered line to avoid committing a partial code line. - if is_inside_unclosed_fence(&source) { - complete_line_count = complete_line_count.saturating_sub(1); - } - } - - if self.committed_line_count >= complete_line_count { - return Vec::new(); - } - - let out_slice = &rendered[self.committed_line_count..complete_line_count]; - // Strong correctness: while a fenced code block is open (no closing fence yet), - // do not emit any new lines from inside it. Wait until the fence closes to emit - // the entire block together. This avoids stray backticks and misformatted content. - if is_inside_unclosed_fence(&source) { - return Vec::new(); - } - - let out = out_slice.to_vec(); - self.committed_line_count = complete_line_count; - out - } - - /// Finalize the stream: emit all remaining lines beyond the last commit. - /// If the buffer does not end with a newline, a temporary one is appended - /// for rendering. Optionally unwraps ```markdown language fences in - /// non-test builds. - pub fn finalize_and_drain(&mut self, config: &Config) -> Vec> { - let mut source: String = self.buffer.clone(); - if !source.ends_with('\n') { - source.push('\n'); - } - let source = unwrap_markdown_language_fence_if_enabled(source); - let source = strip_empty_fenced_code_blocks(&source); - - let mut rendered: Vec> = Vec::new(); - markdown::append_markdown(&source, &mut rendered, config); - - let out = if self.committed_line_count >= rendered.len() { - Vec::new() - } else { - rendered[self.committed_line_count..].to_vec() - }; - - // Reset collector state for next stream. - self.clear(); - out - } -} - -fn is_effectively_empty(line: &Line<'_>) -> bool { - if line.spans.is_empty() { - return true; - } - line.spans - .iter() - .all(|s| s.content.is_empty() || s.content.chars().all(|c| c == ' ')) -} - -/// Remove fenced code blocks that contain no content (whitespace-only) to avoid -/// streaming empty code blocks like ```lang\n``` or ```\n```. -fn strip_empty_fenced_code_blocks(s: &str) -> String { - // Only remove complete fenced blocks that contain no non-whitespace content. - // Leave all other content unchanged to avoid affecting partial streams. - let lines: Vec<&str> = s.lines().collect(); - let mut out = String::with_capacity(s.len()); - let mut i = 0usize; - while i < lines.len() { - let line = lines[i]; - let trimmed_start = line.trim_start(); - let fence_token = if trimmed_start.starts_with("```") { - "```" - } else if trimmed_start.starts_with("~~~") { - "~~~" - } else { - "" - }; - if !fence_token.is_empty() { - // Find a matching closing fence on its own line. - let mut j = i + 1; - let mut has_content = false; - let mut found_close = false; - while j < lines.len() { - let l = lines[j]; - if l.trim() == fence_token { - found_close = true; - break; - } - if !l.trim().is_empty() { - has_content = true; - } - j += 1; - } - if found_close && !has_content { - // Drop i..=j and insert at most a single blank separator line. - if !out.ends_with('\n') { - out.push('\n'); - } - i = j + 1; - continue; - } - // Not an empty fenced block; emit as-is. - out.push_str(line); - out.push('\n'); - i += 1; - } else { - out.push_str(line); - out.push('\n'); - i += 1; - } - } - out -} - -fn is_inside_unclosed_fence(s: &str) -> bool { - let mut open = false; - for line in s.lines() { - let t = line.trim_start(); - if t.starts_with("```") || t.starts_with("~~~") { - if !open { - open = true; - } else { - // closing fence on same pattern toggles off - open = false; - } - } - } - open -} - -#[cfg(test)] -fn unwrap_markdown_language_fence_if_enabled(s: String) -> String { - // In tests, keep content exactly as provided to simplify assertions. - s -} - -#[cfg(not(test))] -fn unwrap_markdown_language_fence_if_enabled(s: String) -> String { - // Best-effort unwrap of a single outer ```markdown fence. - // This is intentionally simple; we can refine as needed later. - const OPEN: &str = "```markdown\n"; - const CLOSE: &str = "\n```\n"; - if s.starts_with(OPEN) && s.ends_with(CLOSE) { - let inner = s[OPEN.len()..s.len() - CLOSE.len()].to_string(); - return inner; - } - s -} - -pub(crate) struct StepResult { - pub history: Vec>, // lines to insert into history this step -} - -/// Streams already-rendered rows into history while computing the newest K -/// rows to show in a live overlay. -pub(crate) struct RenderedLineStreamer { - queue: VecDeque>, -} - -impl RenderedLineStreamer { - pub fn new() -> Self { - Self { - queue: VecDeque::new(), - } - } - - pub fn clear(&mut self) { - self.queue.clear(); - } - - pub fn enqueue(&mut self, lines: Vec>) { - for l in lines { - self.queue.push_back(l); - } - } - - pub fn step(&mut self, _live_max_rows: usize) -> StepResult { - let mut history = Vec::new(); - // Move exactly one per tick to animate gradual insertion. - let burst = if self.queue.is_empty() { 0 } else { 1 }; - for _ in 0..burst { - if let Some(l) = self.queue.pop_front() { - history.push(l); - } - } - - StepResult { history } - } - - pub fn drain_all(&mut self, _live_max_rows: usize) -> StepResult { - let mut history = Vec::new(); - while let Some(l) = self.queue.pop_front() { - history.push(l); - } - StepResult { history } - } - - pub fn is_idle(&self) -> bool { - self.queue.is_empty() - } -} - -#[cfg(test)] -pub(crate) fn simulate_stream_markdown_for_tests( - deltas: &[&str], - finalize: bool, - config: &Config, -) -> Vec> { - let mut collector = MarkdownNewlineCollector::new(); - let mut out = Vec::new(); - for d in deltas { - collector.push_delta(d); - if d.contains('\n') { - out.extend(collector.commit_complete_lines(config)); - } - } - if finalize { - out.extend(collector.finalize_and_drain(config)); - } - out -} - -#[cfg(test)] -mod tests { - use super::*; - use codex_core::config::Config; - use codex_core::config::ConfigOverrides; - - fn test_config() -> Config { - let overrides = ConfigOverrides { - cwd: std::env::current_dir().ok(), - ..Default::default() - }; - match Config::load_with_cli_overrides(vec![], overrides) { - Ok(c) => c, - Err(e) => panic!("load test config: {e}"), - } - } - - #[test] - fn no_commit_until_newline() { - let cfg = test_config(); - let mut c = MarkdownNewlineCollector::new(); - c.push_delta("Hello, world"); - let out = c.commit_complete_lines(&cfg); - assert!(out.is_empty(), "should not commit without newline"); - c.push_delta("!\n"); - let out2 = c.commit_complete_lines(&cfg); - assert_eq!(out2.len(), 1, "one completed line after newline"); - } - - #[test] - fn finalize_commits_partial_line() { - let cfg = test_config(); - let mut c = MarkdownNewlineCollector::new(); - c.push_delta("Line without newline"); - let out = c.finalize_and_drain(&cfg); - assert_eq!(out.len(), 1); - } - - #[test] - fn heading_starts_on_new_line_when_following_paragraph() { - let cfg = test_config(); - - // Stream a paragraph line, then a heading on the next line. - // Expect two distinct rendered lines: "Hello." and "Heading". - let mut c = MarkdownNewlineCollector::new(); - c.push_delta("Hello.\n"); - let out1 = c.commit_complete_lines(&cfg); - let s1: Vec = out1 - .iter() - .map(|l| { - l.spans - .iter() - .map(|s| s.content.clone()) - .collect::>() - .join("") - }) - .collect(); - assert_eq!( - out1.len(), - 1, - "first commit should contain only the paragraph line, got {}: {:?}", - out1.len(), - s1 - ); - - c.push_delta("## Heading\n"); - let out2 = c.commit_complete_lines(&cfg); - let s2: Vec = out2 - .iter() - .map(|l| { - l.spans - .iter() - .map(|s| s.content.clone()) - .collect::>() - .join("") - }) - .collect(); - assert_eq!( - s2, - vec!["", "## Heading"], - "expected a blank separator then the heading line" - ); - - let line_to_string = |l: &ratatui::text::Line<'_>| -> String { - l.spans - .iter() - .map(|s| s.content.clone()) - .collect::>() - .join("") - }; - - assert_eq!(line_to_string(&out1[0]), "Hello."); - assert_eq!(line_to_string(&out2[1]), "## Heading"); - } - - #[test] - fn heading_not_inlined_when_split_across_chunks() { - let cfg = test_config(); - - // Paragraph without trailing newline, then a chunk that starts with the newline - // and the heading text, then a final newline. The collector should first commit - // only the paragraph line, and later commit the heading as its own line. - let mut c = MarkdownNewlineCollector::new(); - c.push_delta("Sounds good!"); - // No commit yet - assert!(c.commit_complete_lines(&cfg).is_empty()); - - // Introduce the newline that completes the paragraph and the start of the heading. - c.push_delta("\n## Adding Bird subcommand"); - let out1 = c.commit_complete_lines(&cfg); - let s1: Vec = out1 - .iter() - .map(|l| { - l.spans - .iter() - .map(|s| s.content.clone()) - .collect::>() - .join("") - }) - .collect(); - assert_eq!( - s1, - vec!["Sounds good!", ""], - "expected paragraph followed by blank separator before heading chunk" - ); - - // Now finish the heading line with the trailing newline. - c.push_delta("\n"); - let out2 = c.commit_complete_lines(&cfg); - let s2: Vec = out2 - .iter() - .map(|l| { - l.spans - .iter() - .map(|s| s.content.clone()) - .collect::>() - .join("") - }) - .collect(); - assert_eq!( - s2, - vec!["## Adding Bird subcommand"], - "expected the heading line only on the final commit" - ); - - // Sanity check raw markdown rendering for a simple line does not produce spurious extras. - let mut rendered: Vec> = Vec::new(); - crate::markdown::append_markdown("Hello.\n", &mut rendered, &cfg); - let rendered_strings: Vec = rendered - .iter() - .map(|l| { - l.spans - .iter() - .map(|s| s.content.clone()) - .collect::>() - .join("") - }) - .collect(); - assert_eq!( - rendered_strings, - vec!["Hello."], - "unexpected markdown lines: {rendered_strings:?}" - ); - - let line_to_string = |l: &ratatui::text::Line<'_>| -> String { - l.spans - .iter() - .map(|s| s.content.clone()) - .collect::>() - .join("") - }; - - assert_eq!(line_to_string(&out1[0]), "Sounds good!"); - assert_eq!(line_to_string(&out1[1]), ""); - assert_eq!(line_to_string(&out2[0]), "## Adding Bird subcommand"); - } - - fn lines_to_plain_strings(lines: &[ratatui::text::Line<'_>]) -> Vec { - lines - .iter() - .map(|l| { - l.spans - .iter() - .map(|s| s.content.clone()) - .collect::>() - .join("") - }) - .collect() - } - - #[test] - fn lists_and_fences_commit_without_duplication() { - let cfg = test_config(); - - // List case - let deltas = vec!["- a\n- ", "b\n- c\n"]; - let streamed = simulate_stream_markdown_for_tests(&deltas, true, &cfg); - let streamed_str = lines_to_plain_strings(&streamed); - - let mut rendered_all: Vec> = Vec::new(); - crate::markdown::append_markdown("- a\n- b\n- c\n", &mut rendered_all, &cfg); - let rendered_all_str = lines_to_plain_strings(&rendered_all); - - assert_eq!( - streamed_str, rendered_all_str, - "list streaming should equal full render without duplication" - ); - - // Fenced code case: stream in small chunks - let deltas2 = vec!["```", "\nco", "de 1\ncode 2\n", "```\n"]; - let streamed2 = simulate_stream_markdown_for_tests(&deltas2, true, &cfg); - let streamed2_str = lines_to_plain_strings(&streamed2); - - let mut rendered_all2: Vec> = Vec::new(); - crate::markdown::append_markdown("```\ncode 1\ncode 2\n```\n", &mut rendered_all2, &cfg); - let rendered_all2_str = lines_to_plain_strings(&rendered_all2); - - assert_eq!( - streamed2_str, rendered_all2_str, - "fence streaming should equal full render without duplication" - ); - } - - #[test] - fn utf8_boundary_safety_and_wide_chars() { - let cfg = test_config(); - - // Emoji (wide), CJK, control char, digit + combining macron sequences - let input = "🙂🙂🙂\n汉字漢字\nA\u{0003}0\u{0304}\n"; - let deltas = vec![ - "🙂", - "🙂", - "🙂\n汉", - "字漢", - "字\nA", - "\u{0003}", - "0", - "\u{0304}", - "\n", - ]; - - let streamed = simulate_stream_markdown_for_tests(&deltas, true, &cfg); - let streamed_str = lines_to_plain_strings(&streamed); - - let mut rendered_all: Vec> = Vec::new(); - crate::markdown::append_markdown(input, &mut rendered_all, &cfg); - let rendered_all_str = lines_to_plain_strings(&rendered_all); - - assert_eq!( - streamed_str, rendered_all_str, - "utf8/wide-char streaming should equal full render without duplication or truncation" - ); - } - - #[test] - fn empty_fenced_block_is_dropped_and_separator_preserved_before_heading() { - let cfg = test_config(); - // An empty fenced code block followed by a heading should not render the fence, - // but should preserve a blank separator line so the heading starts on a new line. - let deltas = vec!["```bash\n```\n", "## Heading\n"]; // empty block and close in same commit - let streamed = simulate_stream_markdown_for_tests(&deltas, true, &cfg); - let texts = lines_to_plain_strings(&streamed); - assert!( - texts.iter().all(|s| !s.contains("```")), - "no fence markers expected: {texts:?}" - ); - // Expect the heading and no fence markers. A blank separator may or may not be rendered at start. - assert!( - texts.iter().any(|s| s == "## Heading"), - "expected heading line: {texts:?}" - ); - } - - #[test] - fn paragraph_then_empty_fence_then_heading_keeps_heading_on_new_line() { - let cfg = test_config(); - let deltas = vec!["Para.\n", "```\n```\n", "## Title\n"]; // empty fence block in one commit - let streamed = simulate_stream_markdown_for_tests(&deltas, true, &cfg); - let texts = lines_to_plain_strings(&streamed); - let para_idx = match texts.iter().position(|s| s == "Para.") { - Some(i) => i, - None => panic!("para present"), - }; - let head_idx = match texts.iter().position(|s| s == "## Title") { - Some(i) => i, - None => panic!("heading present"), - }; - assert!( - head_idx > para_idx, - "heading should not merge with paragraph: {texts:?}" - ); - } -} diff --git a/codex-rs/tui/tests/vt100_history.rs b/codex-rs/tui/tests/vt100_history.rs index 402e847b47..11ee044041 100644 --- a/codex-rs/tui/tests/vt100_history.rs +++ b/codex-rs/tui/tests/vt100_history.rs @@ -75,7 +75,7 @@ impl TestScenario { } #[test] -fn basic_insertion_no_wrap() { +fn hist_001_basic_insertion_no_wrap() { // Screen of 20x6; viewport is the last row (height=1 at y=5) let area = Rect::new(0, 5, 20, 1); let mut scenario = TestScenario::new(20, 6, area); @@ -97,7 +97,7 @@ fn basic_insertion_no_wrap() { } #[test] -fn long_token_wraps() { +fn hist_002_long_token_wraps() { let area = Rect::new(0, 5, 20, 1); let mut scenario = TestScenario::new(20, 6, area); @@ -130,7 +130,7 @@ fn long_token_wraps() { } #[test] -fn emoji_and_cjk() { +fn hist_003_emoji_and_cjk() { let area = Rect::new(0, 5, 20, 1); let mut scenario = TestScenario::new(20, 6, area); @@ -148,7 +148,7 @@ fn emoji_and_cjk() { } #[test] -fn mixed_ansi_spans() { +fn hist_004_mixed_ansi_spans() { let area = Rect::new(0, 5, 20, 1); let mut scenario = TestScenario::new(20, 6, area); @@ -162,7 +162,7 @@ fn mixed_ansi_spans() { } #[test] -fn cursor_restoration() { +fn hist_006_cursor_restoration() { let area = Rect::new(0, 5, 20, 1); let mut scenario = TestScenario::new(20, 6, area); @@ -182,39 +182,7 @@ fn cursor_restoration() { } #[test] -fn word_wrap_no_mid_word_split() { - // Screen of 40x10; viewport is the last row - let area = Rect::new(0, 9, 40, 1); - let mut scenario = TestScenario::new(40, 10, area); - - let sample = "Years passed, and Willowmere thrived in peace and friendship. Mira’s herb garden flourished with both ordinary and enchanted plants, and travelers spoke of the kindness of the woman who tended them."; - let buf = scenario.run_insert(vec![Line::from(sample)]); - let rows = scenario.screen_rows_from_bytes(&buf); - let joined = rows.join("\n"); - assert!( - !joined.contains("bo\nth"), - "word 'both' should not be split across lines:\n{joined}" - ); -} - -#[test] -fn em_dash_and_space_word_wrap() { - // Repro from report: ensure we break before "inside", not mid-word. - let area = Rect::new(0, 9, 40, 1); - let mut scenario = TestScenario::new(40, 10, area); - - let sample = "Mara found an old key on the shore. Curious, she opened a tarnished box half-buried in sand—and inside lay a single, glowing seed."; - let buf = scenario.run_insert(vec![Line::from(sample)]); - let rows = scenario.screen_rows_from_bytes(&buf); - let joined = rows.join("\n"); - assert!( - !joined.contains("insi\nde"), - "word 'inside' should not be split across lines:\n{joined}" - ); -} - -#[test] -fn pre_scroll_region_down() { +fn hist_005_pre_scroll_region_down() { // Viewport not at bottom: y=3 (0-based), height=1 let area = Rect::new(0, 3, 20, 1); let mut scenario = TestScenario::new(20, 6, area); diff --git a/codex-rs/tui/tests/vt100_streaming_no_dup.rs b/codex-rs/tui/tests/vt100_streaming_no_dup.rs deleted file mode 100644 index a359e77a08..0000000000 --- a/codex-rs/tui/tests/vt100_streaming_no_dup.rs +++ /dev/null @@ -1,77 +0,0 @@ -#![cfg(feature = "vt100-tests")] - -use ratatui::backend::TestBackend; -use ratatui::layout::Rect; -use ratatui::text::Line; - -fn term(viewport: Rect) -> codex_tui::custom_terminal::Terminal { - let backend = TestBackend::new(20, 6); - let mut term = codex_tui::custom_terminal::Terminal::with_options(backend) - .unwrap_or_else(|e| panic!("failed to construct terminal: {e}")); - term.set_viewport_area(viewport); - term -} - -#[test] -fn stream_commit_trickle_no_duplication() { - // Viewport is the last row (height=1 at y=5) - let area = Rect::new(0, 5, 20, 1); - let mut t = term(area); - - // Step 1: commit first row - let mut out1 = Vec::new(); - codex_tui::insert_history::insert_history_lines_to_writer( - &mut t, - &mut out1, - vec![Line::from("one")], - ); - - // Step 2: later commit next row - let mut out2 = Vec::new(); - codex_tui::insert_history::insert_history_lines_to_writer( - &mut t, - &mut out2, - vec![Line::from("two")], - ); - - let combined = [out1, out2].concat(); - let s = String::from_utf8_lossy(&combined); - assert_eq!( - s.matches("one").count(), - 1, - "history line duplicated: {s:?}" - ); - assert_eq!( - s.matches("two").count(), - 1, - "history line duplicated: {s:?}" - ); - assert!( - !s.contains("three"), - "live-only content leaked into history: {s:?}" - ); -} - -#[test] -fn live_ring_rows_not_inserted_into_history() { - let area = Rect::new(0, 5, 20, 1); - let mut t = term(area); - - // Commit two rows to history. - let mut buf = Vec::new(); - codex_tui::insert_history::insert_history_lines_to_writer( - &mut t, - &mut buf, - vec![Line::from("one"), Line::from("two")], - ); - - // The live ring might display tail+head rows like ["two", "three"], - // but only committed rows should be present in the history ANSI stream. - let s = String::from_utf8_lossy(&buf); - assert!(s.contains("one")); - assert!(s.contains("two")); - assert!( - !s.contains("three"), - "uncommitted live-ring content should not be inserted into history: {s:?}" - ); -} From 431c9299d495ef771288caf0dcf5952c585f7023 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Thu, 7 Aug 2025 19:01:53 -0700 Subject: [PATCH 0102/1309] Remove part of the error message (#1983) --- codex-rs/core/src/error.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index 7d6dc2cc8d..db2e6be729 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -70,9 +70,7 @@ pub enum CodexErr { )] UsageNotIncluded, - #[error( - "We're currently experiencing high demand, which may cause temporary errors. We’re adding capacity in East and West Europe to restore normal service." - )] + #[error("We're currently experiencing high demand, which may cause temporary errors.")] InternalServerError, /// Retry limit exceeded. From 307d9957fa56ec41706e99ca34417aa64c35eeeb Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Fri, 8 Aug 2025 08:50:44 -0700 Subject: [PATCH 0103/1309] Fix usage limit banner grammar (#2018) ## Summary - fix typo in usage limit banner text - update error message tests ## Testing - `just fmt` - `RUSTC_BOOTSTRAP=1 just fix` *(fails: `let` expressions in this position are unstable)* - `RUSTC_BOOTSTRAP=1 cargo test --all-features` *(fails: `let` expressions in this position are unstable)* ------ https://chatgpt.com/codex/tasks/task_i_689610fc1fe4832081bdd1118779b60b --- codex-rs/core/src/error.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index db2e6be729..2931d30636 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -132,7 +132,7 @@ impl std::fmt::Display for UsageLimitReachedError { } else { write!( f, - "You've hit usage your usage limit. Limits reset every 5h and every week." + "You've hit your usage limit. Limits reset every 5h and every week." )?; } Ok(()) @@ -195,7 +195,7 @@ mod tests { let err = UsageLimitReachedError { plan_type: None }; assert_eq!( err.to_string(), - "You've hit usage your usage limit. Limits reset every 5h and every week." + "You've hit your usage limit. Limits reset every 5h and every week." ); } @@ -206,7 +206,7 @@ mod tests { }; assert_eq!( err.to_string(), - "You've hit usage your usage limit. Limits reset every 5h and every week." + "You've hit your usage limit. Limits reset every 5h and every week." ); } } From c3a8ab8511a0a6a87b438b0febe62bb52ce9cd45 Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Fri, 8 Aug 2025 10:52:24 -0700 Subject: [PATCH 0104/1309] Fix multiline exec command rendering (#2023) With Ratatui, if a single line contains newlines, it increments y but not x so each subsequent line continued from the same x position as the previous line ended on. Before CleanShot 2025-08-08 at 09 13 13 After CleanShot 2025-08-08 at 09 11 54 --- codex-rs/tui/src/history_cell.rs | 35 +++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 443c54aa9b..3658dc7ed7 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -264,14 +264,21 @@ impl HistoryCell { pub(crate) fn new_active_exec_command(command: Vec) -> Self { let command_escaped = strip_bash_lc_and_escape(&command); - let lines: Vec> = vec![ - Line::from(vec![ + let mut lines: Vec> = Vec::new(); + let mut iter = command_escaped.lines(); + if let Some(first) = iter.next() { + lines.push(Line::from(vec![ "▌ ".cyan(), "Running command ".magenta(), - command_escaped.into(), - ]), - Line::from(""), - ]; + first.to_string().into(), + ])); + } else { + lines.push(Line::from(vec!["▌ ".cyan(), "Running command".magenta()])); + } + for cont in iter { + lines.push(Line::from(cont.to_string())); + } + lines.push(Line::from("")); HistoryCell::ActiveExecCommand { view: TextBlock::new(lines), @@ -287,10 +294,18 @@ impl HistoryCell { let mut lines: Vec> = Vec::new(); let command_escaped = strip_bash_lc_and_escape(&command); - lines.push(Line::from(vec![ - "⚡ Ran command ".magenta(), - command_escaped.into(), - ])); + let mut cmd_lines = command_escaped.lines(); + if let Some(first) = cmd_lines.next() { + lines.push(Line::from(vec![ + "⚡ Ran command ".magenta(), + first.to_string().into(), + ])); + } else { + lines.push(Line::from("⚡ Ran command".magenta())); + } + for cont in cmd_lines { + lines.push(Line::from(cont.to_string())); + } let src = if exit_code == 0 { stdout } else { stderr }; From 216e9e2ed0aa9ea1f476ade611c76183d540a7b8 Mon Sep 17 00:00:00 2001 From: Josh LeBlanc Date: Fri, 8 Aug 2025 14:57:16 -0300 Subject: [PATCH 0105/1309] Fix rust build on windows (#2019) This pull request implements a fix from #2000, as well as fixed an additional problem with path lengths on windows that prevents the login from displaying. --------- Co-authored-by: Michael Bolin Co-authored-by: Michael Bolin --- codex-rs/login/Cargo.toml | 1 + codex-rs/login/src/lib.rs | 20 ++++++++++++++++---- codex-rs/tui/src/tui.rs | 10 +++++++--- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/codex-rs/login/Cargo.toml b/codex-rs/login/Cargo.toml index a290c01eb6..85c11505ec 100644 --- a/codex-rs/login/Cargo.toml +++ b/codex-rs/login/Cargo.toml @@ -12,6 +12,7 @@ chrono = { version = "0.4", features = ["serde"] } reqwest = { version = "0.12", features = ["json"] } serde = { version = "1", features = ["derive"] } serde_json = "1" +tempfile = "3" thiserror = "2.0.12" tokio = { version = "1", features = [ "io-std", diff --git a/codex-rs/login/src/lib.rs b/codex-rs/login/src/lib.rs index 2a8f6749b4..f25f885bdc 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -18,6 +18,7 @@ use std::process::Stdio; use std::sync::Arc; use std::sync::Mutex; use std::time::Duration; +use tempfile::NamedTempFile; use tokio::process::Command; pub use crate::token_data::TokenData; @@ -263,9 +264,9 @@ pub struct SpawnedLogin { /// Spawn the ChatGPT login Python server as a child process and return a handle to its process. pub fn spawn_login_with_chatgpt(codex_home: &Path) -> std::io::Result { + let script_path = write_login_script_to_disk()?; let mut cmd = std::process::Command::new("python3"); - cmd.arg("-c") - .arg(SOURCE_FOR_PYTHON_SERVER) + cmd.arg(&script_path) .env("CODEX_HOME", codex_home) .env("CODEX_CLIENT_ID", CLIENT_ID) .stdin(Stdio::null()) @@ -315,9 +316,9 @@ pub fn spawn_login_with_chatgpt(codex_home: &Path) -> std::io::Result std::io::Result<()> { + let script_path = write_login_script_to_disk()?; let child = Command::new("python3") - .arg("-c") - .arg(SOURCE_FOR_PYTHON_SERVER) + .arg(&script_path) .env("CODEX_HOME", codex_home) .env("CODEX_CLIENT_ID", CLIENT_ID) .stdin(Stdio::null()) @@ -344,6 +345,17 @@ pub async fn login_with_chatgpt(codex_home: &Path, capture_output: bool) -> std: } } +fn write_login_script_to_disk() -> std::io::Result { + // Write the embedded Python script to a file to avoid very long + // command-line arguments (Windows error 206). + let mut tmp = NamedTempFile::new()?; + tmp.write_all(SOURCE_FOR_PYTHON_SERVER.as_bytes())?; + tmp.flush()?; + + let (_file, path) = tmp.keep()?; + Ok(path) +} + pub fn login_with_api_key(codex_home: &Path, api_key: &str) -> std::io::Result<()> { let auth_dot_json = AuthDotJson { openai_api_key: Some(api_key.to_string()), diff --git a/codex-rs/tui/src/tui.rs b/codex-rs/tui/src/tui.rs index e0bf9bcc57..0447e32ae9 100644 --- a/codex-rs/tui/src/tui.rs +++ b/codex-rs/tui/src/tui.rs @@ -29,14 +29,17 @@ pub fn init(_config: &Config) -> Result { // Enable keyboard enhancement flags so modifiers for keys like Enter are disambiguated. // chat_composer.rs is using a keyboard event listener to enter for any modified keys // to create a new line that require this. - execute!( + // Some terminals (notably legacy Windows consoles) do not support + // keyboard enhancement flags. Attempt to enable them, but continue + // gracefully if unsupported. + let _ = execute!( stdout(), PushKeyboardEnhancementFlags( KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES | KeyboardEnhancementFlags::REPORT_EVENT_TYPES | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS ) - )?; + ); set_panic_hook(); // Clear screen and move cursor to top-left before drawing UI @@ -57,7 +60,8 @@ fn set_panic_hook() { /// Restore the terminal to its original state pub fn restore() -> Result<()> { - execute!(stdout(), PopKeyboardEnhancementFlags)?; + // Pop may fail on platforms that didn't support the push; ignore errors. + let _ = execute!(stdout(), PopKeyboardEnhancementFlags); execute!(stdout(), DisableBracketedPaste)?; disable_raw_mode()?; Ok(()) From 6cfee156121cc08e211e1f5f6e68a6f68bb92bb8 Mon Sep 17 00:00:00 2001 From: aibrahim-oai Date: Fri, 8 Aug 2025 12:43:43 -0700 Subject: [PATCH 0106/1309] Moving the compact prompt near where it's used (#2031) - Moved the prompt for compact to core - Renamed it to be more clear --- codex-rs/core/src/codex.rs | 2 +- SUMMARY.md => codex-rs/core/src/prompt_for_compact_command.md | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename SUMMARY.md => codex-rs/core/src/prompt_for_compact_command.md (100%) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 385361e8ff..ada5b2886f 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1018,7 +1018,7 @@ async fn submission_loop( }; // Create a summarization request as user input - const SUMMARIZATION_PROMPT: &str = include_str!("../../../SUMMARY.md"); + const SUMMARIZATION_PROMPT: &str = include_str!("prompt_for_compact_command.md"); // Attempt to inject input into current task if let Err(items) = sess.inject_input(vec![InputItem::Text { diff --git a/SUMMARY.md b/codex-rs/core/src/prompt_for_compact_command.md similarity index 100% rename from SUMMARY.md rename to codex-rs/core/src/prompt_for_compact_command.md From 18eb15700021f0a0cab861c4540212fd161da231 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 8 Aug 2025 13:03:11 -0700 Subject: [PATCH 0107/1309] feat: include windows binaries in GitHub releases (#2035) We should stop shipping the old TypeScript CLI to Windows users. I did some light testing of the Rust CLI on Windows in `cmd.exe` and it works better than I expected! --- .github/workflows/rust-release.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 812d7a3cfe..32c9669110 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -70,6 +70,8 @@ jobs: target: aarch64-unknown-linux-musl - runner: ubuntu-24.04-arm target: aarch64-unknown-linux-gnu + - runner: windows-latest + target: x86_64-pc-windows-msvc steps: - uses: actions/checkout@v4 @@ -101,8 +103,13 @@ jobs: dest="dist/${{ matrix.target }}" mkdir -p "$dest" - cp target/${{ matrix.target }}/release/codex-exec "$dest/codex-exec-${{ matrix.target }}" - cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}" + if [[ "${{ matrix.runner }}" == windows* ]]; then + cp target/${{ matrix.target }}/release/codex-exec.exe "$dest/codex-exec-${{ matrix.target }}.exe" + cp target/${{ matrix.target }}/release/codex.exe "$dest/codex-${{ matrix.target }}.exe" + else + cp target/${{ matrix.target }}/release/codex-exec "$dest/codex-exec-${{ matrix.target }}" + cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}" + fi # After https://github.com/openai/codex/pull/1228 is merged and a new # release is cut with an artifacts built after that PR, the `-gnu` From 8a26ea0fe0e2c348bf3673241732283a7233a02a Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 8 Aug 2025 13:42:33 -0700 Subject: [PATCH 0108/1309] fix: stop building codex-exec and codex-linux-sandbox binaries (#2036) Release builds are taking awhile and part of the reason that we are building binaries that we are not really using. Adding Windows binaries into releases (https://github.com/openai/codex/pull/2035) slows things down, so we need to get some time back. - `codex-exec` is basically a standalone `codex exec` that we were offering because it's a bit smaller as it does not include all the bits to power the TUI. We were using it in our experimental GitHub Action, so this PR updates the Action to use `codex exec` instead. - `codex-linux-sandbox` was a helper binary for the TypeScript version of the CLI, but I am about to axe that, so we don't need this either. If we decide to bring `codex-exec` back at some point, we should use a separate instances so we can build it in parallel with `codex`. (I think if we had beefier build machines, this wouldn't be so bad, but that's not the case with the default runners from GitHub.) --- .github/actions/codex/action.yml | 12 ++++----- .github/actions/codex/src/run-codex.ts | 4 ++- .github/dotslash-config.json | 36 ++++++++++++-------------- .github/workflows/rust-release.yml | 15 +---------- 4 files changed, 25 insertions(+), 42 deletions(-) diff --git a/.github/actions/codex/action.yml b/.github/actions/codex/action.yml index 404194c00f..011cbccfdf 100644 --- a/.github/actions/codex/action.yml +++ b/.github/actions/codex/action.yml @@ -82,20 +82,18 @@ runs: # Note that if we start baking version numbers into the artifact name, # we will need to update this action.yml file to match. - artifact="codex-exec-${triple}.tar.gz" + artifact="codex-${triple}.tar.gz" TAG_ARG="${{ inputs.codex_release_tag }}" # The usage is `gh release download [] [flags]`, so if TAG_ARG # is empty, we do not pass it so we can default to the latest release. gh release download ${TAG_ARG:+$TAG_ARG} --repo openai/codex \ --pattern "$artifact" --output - \ - | tar xzO > /usr/local/bin/codex-exec - chmod +x /usr/local/bin/codex-exec + | tar xzO > /usr/local/bin/codex + chmod +x /usr/local/bin/codex - # Display Codex version to confirm binary integrity; ensure we point it - # at the checked-out repository via --cd so that any subsequent commands - # use the correct working directory. - codex-exec --cd "$GITHUB_WORKSPACE" --version + # Display Codex version to confirm binary integrity. + codex --version - name: Install Bun uses: oven-sh/setup-bun@v2 diff --git a/.github/actions/codex/src/run-codex.ts b/.github/actions/codex/src/run-codex.ts index 2c851823e8..3c0255e2ec 100644 --- a/.github/actions/codex/src/run-codex.ts +++ b/.github/actions/codex/src/run-codex.ts @@ -18,7 +18,9 @@ export async function runCodex( const tempDirPath = await mkdtemp(join(tmpdir(), "codex-")); const lastMessageOutput = join(tempDirPath, "codex-prompt.md"); - const args = ["/usr/local/bin/codex-exec"]; + // Use the unified CLI and its `exec` subcommand instead of the old + // standalone `codex-exec` binary. + const args = ["/usr/local/bin/codex", "exec"]; const inputCodexArgs = ctx.tryGet("INPUT_CODEX_ARGS")?.trim(); if (inputCodexArgs) { diff --git a/.github/dotslash-config.json b/.github/dotslash-config.json index 1e32001e66..82b9eb93b3 100644 --- a/.github/dotslash-config.json +++ b/.github/dotslash-config.json @@ -1,27 +1,23 @@ { "outputs": { - "codex-exec": { - "platforms": { - "macos-aarch64": { "regex": "^codex-exec-aarch64-apple-darwin\\.zst$", "path": "codex-exec" }, - "macos-x86_64": { "regex": "^codex-exec-x86_64-apple-darwin\\.zst$", "path": "codex-exec" }, - "linux-x86_64": { "regex": "^codex-exec-x86_64-unknown-linux-musl\\.zst$", "path": "codex-exec" }, - "linux-aarch64": { "regex": "^codex-exec-aarch64-unknown-linux-musl\\.zst$", "path": "codex-exec" } - } - }, - "codex": { "platforms": { - "macos-aarch64": { "regex": "^codex-aarch64-apple-darwin\\.zst$", "path": "codex" }, - "macos-x86_64": { "regex": "^codex-x86_64-apple-darwin\\.zst$", "path": "codex" }, - "linux-x86_64": { "regex": "^codex-x86_64-unknown-linux-musl\\.zst$", "path": "codex" }, - "linux-aarch64": { "regex": "^codex-aarch64-unknown-linux-musl\\.zst$", "path": "codex" } - } - }, - - "codex-linux-sandbox": { - "platforms": { - "linux-x86_64": { "regex": "^codex-linux-sandbox-x86_64-unknown-linux-musl\\.zst$", "path": "codex-linux-sandbox" }, - "linux-aarch64": { "regex": "^codex-linux-sandbox-aarch64-unknown-linux-musl\\.zst$", "path": "codex-linux-sandbox" } + "macos-aarch64": { + "regex": "^codex-aarch64-apple-darwin\\.zst$", + "path": "codex" + }, + "macos-x86_64": { + "regex": "^codex-x86_64-apple-darwin\\.zst$", + "path": "codex" + }, + "linux-x86_64": { + "regex": "^codex-x86_64-unknown-linux-musl\\.zst$", + "path": "codex" + }, + "linux-aarch64": { + "regex": "^codex-aarch64-unknown-linux-musl\\.zst$", + "path": "codex" + } } } } diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 32c9669110..ea4a3574fa 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -95,7 +95,7 @@ jobs: sudo apt install -y musl-tools pkg-config - name: Cargo build - run: cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-exec --bin codex-linux-sandbox + run: cargo build --target ${{ matrix.target }} --release --bin codex - name: Stage artifacts shell: bash @@ -104,23 +104,11 @@ jobs: mkdir -p "$dest" if [[ "${{ matrix.runner }}" == windows* ]]; then - cp target/${{ matrix.target }}/release/codex-exec.exe "$dest/codex-exec-${{ matrix.target }}.exe" cp target/${{ matrix.target }}/release/codex.exe "$dest/codex-${{ matrix.target }}.exe" else - cp target/${{ matrix.target }}/release/codex-exec "$dest/codex-exec-${{ matrix.target }}" cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}" fi - # After https://github.com/openai/codex/pull/1228 is merged and a new - # release is cut with an artifacts built after that PR, the `-gnu` - # variants can go away as we will only use the `-musl` variants. - - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'x86_64-unknown-linux-gnu' || matrix.target == 'aarch64-unknown-linux-gnu' || matrix.target == 'aarch64-unknown-linux-musl' }} - name: Stage Linux-only artifacts - shell: bash - run: | - dest="dist/${{ matrix.target }}" - cp target/${{ matrix.target }}/release/codex-linux-sandbox "$dest/codex-linux-sandbox-${{ matrix.target }}" - - name: Compress artifacts shell: bash run: | @@ -133,7 +121,6 @@ jobs: # we publish. The end result is: # codex-.zst (existing) # codex-.tar.gz (new) - # ...same naming for codex-exec-* and codex-linux-sandbox-* # 1. Produce a .tar.gz for every file in the directory *before* we # run `zstd --rm`, because that flag deletes the original files. From d0cf0367995ebe437d314d709b6ac1ae5716ed9c Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 8 Aug 2025 14:44:35 -0700 Subject: [PATCH 0109/1309] feat: include Windows binary of the CLI in the npm release (#2040) To date, the build scripts in `codex-cli` still supported building the old TypeScript version of the Codex CLI to give Windows users something they can run, but we are just going to have them use the Rust version like everyone else, so: - updates `codex-cli/bin/codex.js` so that we run the native binary or throw if the target platform/arch is not supported (no more conditional usage based on `CODEX_RUST`, `use-native` file, etc.) - drops the `--native` flag from `codex-cli/scripts/stage_release.sh` and updates all the code paths to behave as if `--native` were passed (i.e., it is the only way to run it now) Tested this by running: ``` ./codex-cli/scripts/stage_rust_release.py --release-version 0.20.0-alpha.2 ``` --- codex-cli/bin/codex.js | 251 ++++++++++------------- codex-cli/scripts/install_native_deps.sh | 49 ++--- codex-cli/scripts/stage_release.sh | 38 +--- codex-cli/scripts/stage_rust_release.py | 1 - 4 files changed, 133 insertions(+), 206 deletions(-) diff --git a/codex-cli/bin/codex.js b/codex-cli/bin/codex.js index df06dd36a7..d92d8f2f4f 100755 --- a/codex-cli/bin/codex.js +++ b/codex-cli/bin/codex.js @@ -1,154 +1,123 @@ #!/usr/bin/env node // Unified entry point for the Codex CLI. -/* - * Behavior - * ========= - * 1. By default we import the JavaScript implementation located in - * dist/cli.js. - * - * 2. Developers can opt-in to a pre-compiled Rust binary by setting the - * environment variable CODEX_RUST to a truthy value (`1`, `true`, etc.). - * When that variable is present we resolve the correct binary for the - * current platform / architecture and execute it via child_process. - * - * If the CODEX_RUST=1 is specified and there is no native binary for the - * current platform / architecture, an error is thrown. - */ -import fs from "fs"; import path from "path"; -import { fileURLToPath, pathToFileURL } from "url"; - -// Determine whether the user explicitly wants the Rust CLI. +import { fileURLToPath } from "url"; // __dirname equivalent in ESM const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -// For the @native release of the Node module, the `use-native` file is added, -// indicating we should default to the native binary. For other releases, -// setting CODEX_RUST=1 will opt-in to the native binary, if included. -const wantsNative = fs.existsSync(path.join(__dirname, "use-native")) || - (process.env.CODEX_RUST != null - ? ["1", "true", "yes"].includes(process.env.CODEX_RUST.toLowerCase()) - : false); +const { platform, arch } = process; -// Try native binary if requested. -if (wantsNative && process.platform !== 'win32') { - const { platform, arch } = process; - - let targetTriple = null; - switch (platform) { - case "linux": - case "android": - switch (arch) { - case "x64": - targetTriple = "x86_64-unknown-linux-musl"; - break; - case "arm64": - targetTriple = "aarch64-unknown-linux-musl"; - break; - default: - break; - } - break; - case "darwin": - switch (arch) { - case "x64": - targetTriple = "x86_64-apple-darwin"; - break; - case "arm64": - targetTriple = "aarch64-apple-darwin"; - break; - default: - break; - } - break; - default: - break; - } - - if (!targetTriple) { - throw new Error(`Unsupported platform: ${platform} (${arch})`); - } - - const binaryPath = path.join(__dirname, "..", "bin", `codex-${targetTriple}`); - - // Use an asynchronous spawn instead of spawnSync so that Node is able to - // respond to signals (e.g. Ctrl-C / SIGINT) while the native binary is - // executing. This allows us to forward those signals to the child process - // and guarantees that when either the child terminates or the parent - // receives a fatal signal, both processes exit in a predictable manner. - const { spawn } = await import("child_process"); - - const child = spawn(binaryPath, process.argv.slice(2), { - stdio: "inherit", - env: { ...process.env, CODEX_MANAGED_BY_NPM: "1" }, - }); - - child.on("error", (err) => { - // Typically triggered when the binary is missing or not executable. - // Re-throwing here will terminate the parent with a non-zero exit code - // while still printing a helpful stack trace. - // eslint-disable-next-line no-console - console.error(err); - process.exit(1); - }); - - // Forward common termination signals to the child so that it shuts down - // gracefully. In the handler we temporarily disable the default behavior of - // exiting immediately; once the child has been signaled we simply wait for - // its exit event which will in turn terminate the parent (see below). - const forwardSignal = (signal) => { - if (child.killed) { - return; +let targetTriple = null; +switch (platform) { + case "linux": + case "android": + switch (arch) { + case "x64": + targetTriple = "x86_64-unknown-linux-musl"; + break; + case "arm64": + targetTriple = "aarch64-unknown-linux-musl"; + break; + default: + break; } - try { - child.kill(signal); - } catch { - /* ignore */ + break; + case "darwin": + switch (arch) { + case "x64": + targetTriple = "x86_64-apple-darwin"; + break; + case "arm64": + targetTriple = "aarch64-apple-darwin"; + break; + default: + break; } - }; - - ["SIGINT", "SIGTERM", "SIGHUP"].forEach((sig) => { - process.on(sig, () => forwardSignal(sig)); - }); - - // When the child exits, mirror its termination reason in the parent so that - // shell scripts and other tooling observe the correct exit status. - // Wrap the lifetime of the child process in a Promise so that we can await - // its termination in a structured way. The Promise resolves with an object - // describing how the child exited: either via exit code or due to a signal. - const childResult = await new Promise((resolve) => { - child.on("exit", (code, signal) => { - if (signal) { - resolve({ type: "signal", signal }); - } else { - resolve({ type: "code", exitCode: code ?? 1 }); - } - }); - }); - - if (childResult.type === "signal") { - // Re-emit the same signal so that the parent terminates with the expected - // semantics (this also sets the correct exit code of 128 + n). - process.kill(process.pid, childResult.signal); - } else { - process.exit(childResult.exitCode); - } -} else { - // Fallback: execute the original JavaScript CLI. - - // Resolve the path to the compiled CLI bundle - const cliPath = path.resolve(__dirname, "../dist/cli.js"); - const cliUrl = pathToFileURL(cliPath).href; - - // Load and execute the CLI - try { - await import(cliUrl); - } catch (err) { - // eslint-disable-next-line no-console - console.error(err); - process.exit(1); - } + break; + case "win32": + switch (arch) { + case "x64": + targetTriple = "x86_64-pc-windows-msvc.exe"; + break; + case "arm64": + // We do not build this today, fall through... + default: + break; + } + break; + default: + break; } + +if (!targetTriple) { + throw new Error(`Unsupported platform: ${platform} (${arch})`); +} + +const binaryPath = path.join(__dirname, "..", "bin", `codex-${targetTriple}`); + +// Use an asynchronous spawn instead of spawnSync so that Node is able to +// respond to signals (e.g. Ctrl-C / SIGINT) while the native binary is +// executing. This allows us to forward those signals to the child process +// and guarantees that when either the child terminates or the parent +// receives a fatal signal, both processes exit in a predictable manner. +const { spawn } = await import("child_process"); + +const child = spawn(binaryPath, process.argv.slice(2), { + stdio: "inherit", + env: { ...process.env, CODEX_MANAGED_BY_NPM: "1" }, +}); + +child.on("error", (err) => { + // Typically triggered when the binary is missing or not executable. + // Re-throwing here will terminate the parent with a non-zero exit code + // while still printing a helpful stack trace. + // eslint-disable-next-line no-console + console.error(err); + process.exit(1); +}); + +// Forward common termination signals to the child so that it shuts down +// gracefully. In the handler we temporarily disable the default behavior of +// exiting immediately; once the child has been signaled we simply wait for +// its exit event which will in turn terminate the parent (see below). +const forwardSignal = (signal) => { + if (child.killed) { + return; + } + try { + child.kill(signal); + } catch { + /* ignore */ + } +}; + +["SIGINT", "SIGTERM", "SIGHUP"].forEach((sig) => { + process.on(sig, () => forwardSignal(sig)); +}); + +// When the child exits, mirror its termination reason in the parent so that +// shell scripts and other tooling observe the correct exit status. +// Wrap the lifetime of the child process in a Promise so that we can await +// its termination in a structured way. The Promise resolves with an object +// describing how the child exited: either via exit code or due to a signal. +const childResult = await new Promise((resolve) => { + child.on("exit", (code, signal) => { + if (signal) { + resolve({ type: "signal", signal }); + } else { + resolve({ type: "code", exitCode: code ?? 1 }); + } + }); +}); + +if (childResult.type === "signal") { + // Re-emit the same signal so that the parent terminates with the expected + // semantics (this also sets the correct exit code of 128 + n). + process.kill(process.pid, childResult.signal); +} else { + process.exit(childResult.exitCode); +} + diff --git a/codex-cli/scripts/install_native_deps.sh b/codex-cli/scripts/install_native_deps.sh index 353ffafdba..6cf2faafc8 100755 --- a/codex-cli/scripts/install_native_deps.sh +++ b/codex-cli/scripts/install_native_deps.sh @@ -2,13 +2,8 @@ # Install native runtime dependencies for codex-cli. # -# By default the script copies the sandbox binaries that are required at -# runtime. When called with the --full-native flag, it additionally -# bundles pre-built Rust CLI binaries so that the resulting npm package can run -# the native implementation when users set CODEX_RUST=1. -# # Usage -# install_native_deps.sh [--full-native] [--workflow-url URL] [CODEX_CLI_ROOT] +# install_native_deps.sh [--workflow-url URL] [CODEX_CLI_ROOT] # # The optional RELEASE_ROOT is the path that contains package.json. Omitting # it installs the binaries into the repository's own bin/ folder to support @@ -21,18 +16,14 @@ set -euo pipefail # ------------------ CODEX_CLI_ROOT="" -INCLUDE_RUST=0 # Until we start publishing stable GitHub releases, we have to grab the binaries # from the GitHub Action that created them. Update the URL below to point to the # appropriate workflow run: -WORKFLOW_URL="https://github.com/openai/codex/actions/runs/15981617627" +WORKFLOW_URL="https://github.com/openai/codex/actions/runs/16840150768" # rust-v0.20.0-alpha.2 while [[ $# -gt 0 ]]; do case "$1" in - --full-native) - INCLUDE_RUST=1 - ;; --workflow-url) shift || { echo "--workflow-url requires an argument"; exit 1; } if [ -n "$1" ]; then @@ -81,26 +72,20 @@ trap 'rm -rf "$ARTIFACTS_DIR"' EXIT # NB: The GitHub CLI `gh` must be installed and authenticated. gh run download --dir "$ARTIFACTS_DIR" --repo openai/codex "$WORKFLOW_ID" -# Decompress the artifacts for Linux sandboxing. -zstd -d "$ARTIFACTS_DIR/x86_64-unknown-linux-musl/codex-linux-sandbox-x86_64-unknown-linux-musl.zst" \ - -o "$BIN_DIR/codex-linux-sandbox-x64" - -zstd -d "$ARTIFACTS_DIR/aarch64-unknown-linux-musl/codex-linux-sandbox-aarch64-unknown-linux-musl.zst" \ - -o "$BIN_DIR/codex-linux-sandbox-arm64" - -if [[ "$INCLUDE_RUST" -eq 1 ]]; then - # x64 Linux - zstd -d "$ARTIFACTS_DIR/x86_64-unknown-linux-musl/codex-x86_64-unknown-linux-musl.zst" \ - -o "$BIN_DIR/codex-x86_64-unknown-linux-musl" - # ARM64 Linux - zstd -d "$ARTIFACTS_DIR/aarch64-unknown-linux-musl/codex-aarch64-unknown-linux-musl.zst" \ - -o "$BIN_DIR/codex-aarch64-unknown-linux-musl" - # x64 macOS - zstd -d "$ARTIFACTS_DIR/x86_64-apple-darwin/codex-x86_64-apple-darwin.zst" \ - -o "$BIN_DIR/codex-x86_64-apple-darwin" - # ARM64 macOS - zstd -d "$ARTIFACTS_DIR/aarch64-apple-darwin/codex-aarch64-apple-darwin.zst" \ - -o "$BIN_DIR/codex-aarch64-apple-darwin" -fi +# x64 Linux +zstd -d "$ARTIFACTS_DIR/x86_64-unknown-linux-musl/codex-x86_64-unknown-linux-musl.zst" \ + -o "$BIN_DIR/codex-x86_64-unknown-linux-musl" +# ARM64 Linux +zstd -d "$ARTIFACTS_DIR/aarch64-unknown-linux-musl/codex-aarch64-unknown-linux-musl.zst" \ + -o "$BIN_DIR/codex-aarch64-unknown-linux-musl" +# x64 macOS +zstd -d "$ARTIFACTS_DIR/x86_64-apple-darwin/codex-x86_64-apple-darwin.zst" \ + -o "$BIN_DIR/codex-x86_64-apple-darwin" +# ARM64 macOS +zstd -d "$ARTIFACTS_DIR/aarch64-apple-darwin/codex-aarch64-apple-darwin.zst" \ + -o "$BIN_DIR/codex-aarch64-apple-darwin" +# x64 Windows +zstd -d "$ARTIFACTS_DIR/x86_64-pc-windows-msvc/codex-x86_64-pc-windows-msvc.exe.zst" \ + -o "$BIN_DIR/codex-x86_64-pc-windows-msvc.exe" echo "Installed native dependencies into $BIN_DIR" diff --git a/codex-cli/scripts/stage_release.sh b/codex-cli/scripts/stage_release.sh index cd32ade6f9..bc2dee1436 100755 --- a/codex-cli/scripts/stage_release.sh +++ b/codex-cli/scripts/stage_release.sh @@ -7,15 +7,8 @@ # Usage: # # --tmp : Use instead of a freshly created temp directory. -# --native : Bundle the pre-built Rust CLI binaries for Linux alongside -# the JavaScript implementation (a so-called "fat" package). # -h|--help : Print usage. # -# When --native is supplied we copy the linux-sandbox binaries (as before) and -# additionally fetch / unpack the two Rust targets that we currently support: -# - x86_64-unknown-linux-musl -# - aarch64-unknown-linux-musl -# # NOTE: This script is intended to be run from the repository root via # `pnpm --filter codex-cli stage-release ...` or inside codex-cli with the # helper script entry in package.json (`pnpm stage-release ...`). @@ -27,11 +20,10 @@ set -euo pipefail usage() { cat </dev/null echo "Staged version $VERSION for release in $TMPDIR" -if [[ "$INCLUDE_NATIVE" -eq 1 ]]; then - echo "Verify the CLI:" - echo " node ${TMPDIR}/bin/codex.js --version" - echo " node ${TMPDIR}/bin/codex.js --help" -else - echo "Test Node:" - echo " node ${TMPDIR}/bin/codex.js --help" -fi +echo "Verify the CLI:" +echo " node ${TMPDIR}/bin/codex.js --version" +echo " node ${TMPDIR}/bin/codex.js --help" # Print final hint for convenience -if [[ "$INCLUDE_NATIVE" -eq 1 ]]; then - echo "Next: cd \"$TMPDIR\" && npm publish --tag native" -else - echo "Next: cd \"$TMPDIR\" && npm publish" -fi +echo "Next: cd \"$TMPDIR\" && npm publish" diff --git a/codex-cli/scripts/stage_rust_release.py b/codex-cli/scripts/stage_rust_release.py index 6d1326af92..a2f42e224f 100755 --- a/codex-cli/scripts/stage_rust_release.py +++ b/codex-cli/scripts/stage_rust_release.py @@ -50,7 +50,6 @@ Run this after the GitHub Release has been created and use version, "--workflow-url", workflow["url"], - "--native", ] ) stage_release.check_returncode() From 33f266dab35a90b55c42fd2dd845427756ae9b21 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Fri, 8 Aug 2025 15:15:35 -0700 Subject: [PATCH 0110/1309] Use certifi certificate when available (#2042) certifi has a more consistent set of Mozilla maintained root certificates --- codex-rs/login/src/login_with_chatgpt.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/codex-rs/login/src/login_with_chatgpt.py b/codex-rs/login/src/login_with_chatgpt.py index 317c95769c..ddcc6e66c7 100644 --- a/codex-rs/login/src/login_with_chatgpt.py +++ b/codex-rs/login/src/login_with_chatgpt.py @@ -44,6 +44,15 @@ DEFAULT_ISSUER = "https://auth.openai.com" EXIT_CODE_WHEN_ADDRESS_ALREADY_IN_USE = 13 +CA_CONTEXT = None +try: + import ssl + import certifi as _certifi + + CA_CONTEXT = ssl.create_default_context(cafile=_certifi.where()) +except Exception: + pass + @dataclass class TokenData: @@ -255,7 +264,8 @@ class _ApiKeyHTTPHandler(http.server.BaseHTTPRequestHandler): data=exchange_data, method="POST", headers={"Content-Type": "application/x-www-form-urlencoded"}, - ) + ), + context=CA_CONTEXT, ) as resp: exchange_payload = json.loads(resp.read().decode()) exchanged_access_token = exchange_payload["access_token"] @@ -326,7 +336,8 @@ class _ApiKeyHTTPHandler(http.server.BaseHTTPRequestHandler): data=data, method="POST", headers={"Content-Type": "application/x-www-form-urlencoded"}, - ) + ), + context=CA_CONTEXT, ) as resp: payload = json.loads(resp.read().decode()) @@ -506,7 +517,7 @@ def maybe_redeem_credits( headers={"Content-Type": "application/json"}, ) - with urllib.request.urlopen(req) as resp: + with urllib.request.urlopen(req, context=CA_CONTEXT) as resp: refresh_data = json.loads(resp.read().decode()) new_id_token = refresh_data.get("id_token") new_id_claims = parse_id_token_claims(new_id_token or "") @@ -596,7 +607,7 @@ def maybe_redeem_credits( headers={"Content-Type": "application/json"}, ) - with urllib.request.urlopen(req) as resp: + with urllib.request.urlopen(req, context=CA_CONTEXT) as resp: redeem_data = json.loads(resp.read().decode()) granted = redeem_data.get("granted_chatgpt_subscriber_api_credits", 0) From 39a4d4ed8e15bc85a7e80c33b5a99453d9cf60e9 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 8 Aug 2025 15:17:54 -0700 Subject: [PATCH 0111/1309] fix: try building the npm package in CI (#2043) Historically, the release process for the npm module has been: - I run `codex-rs/scripts/create_github_release.sh` to kick off a release for the native artifacts. - I wait until it is done. - I run `codex-cli/scripts/stage_rust_release.py` to build the npm release locally - I run `npm publish` from my laptop It has been a longstanding issue to move the npm build to CI. I may still have to do the `npm publish` manually because it requires 2fac with `npm`, though I assume we can work that out later. Note I asked Codex to make these updates, and while they look pretty good to me, I'm not 100% certain, but let's just merge this and I'll kick off another alpha build and we'll see what happens? --- .github/workflows/rust-release.yml | 41 +++++++++++++++++++++++++ codex-cli/scripts/stage_rust_release.py | 27 ++++++++++------ 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index ea4a3574fa..fc0938e20b 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -154,6 +154,9 @@ jobs: runs-on: ubuntu-latest steps: + - name: Checkout repository + uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 with: path: dist @@ -169,6 +172,44 @@ jobs: version="${GITHUB_REF_NAME#rust-v}" echo "name=${version}" >> $GITHUB_OUTPUT + # Setup Node + pnpm similar to ci.yml so we can build the npm package + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.8.1 + run_install: false + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: | + echo "store_path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-cache.outputs.store_path }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Stage npm package + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + TMP_DIR="${RUNNER_TEMP}/npm-stage" + python3 codex-cli/scripts/stage_rust_release.py \ + --release-version "${{ steps.release_name.outputs.name }}" \ + --tmp "${TMP_DIR}" + mkdir -p dist/npm + (cd "$TMP_DIR" && zip -r "${GITHUB_WORKSPACE}/dist/npm/codex-npm-${{ steps.release_name.outputs.name }}.zip" .) + - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: diff --git a/codex-cli/scripts/stage_rust_release.py b/codex-cli/scripts/stage_rust_release.py index a2f42e224f..9a554b77d0 100755 --- a/codex-cli/scripts/stage_rust_release.py +++ b/codex-cli/scripts/stage_rust_release.py @@ -13,11 +13,18 @@ def main() -> int: Run this after the GitHub Release has been created and use `--release-version` to specify the version to release. + +Optionally pass `--tmp` to control the temporary staging directory that will be +forwarded to stage_release.sh. """ ) parser.add_argument( "--release-version", required=True, help="Version to release, e.g., 0.3.0" ) + parser.add_argument( + "--tmp", + help="Optional path to stage the npm package; forwarded to stage_release.sh", + ) args = parser.parse_args() version = args.release_version @@ -43,15 +50,17 @@ Run this after the GitHub Release has been created and use print(f"should `git checkout {sha}`") current_dir = Path(__file__).parent.resolve() - stage_release = subprocess.run( - [ - current_dir / "stage_release.sh", - "--version", - version, - "--workflow-url", - workflow["url"], - ] - ) + cmd = [ + str(current_dir / "stage_release.sh"), + "--version", + version, + "--workflow-url", + workflow["url"], + ] + if args.tmp: + cmd.extend(["--tmp", args.tmp]) + + stage_release = subprocess.run(cmd) stage_release.check_returncode() return 0 From 75febbdefa8ebf8a1db80b79af1489c7dca73738 Mon Sep 17 00:00:00 2001 From: Dylan Date: Fri, 8 Aug 2025 15:19:20 -0700 Subject: [PATCH 0112/1309] Update README.md (#1989) Updates the README to clarify auth vs. api key behavior. --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5ee34f6cd1..388777db1a 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,10 @@ If you prefer to pay-as-you-go, you can still authenticate with your OpenAI API export OPENAI_API_KEY="your-api-key-here" ``` -> Note: This command only sets the key for your current terminal session, which we recommend. To set it for all future sessions, you can also add the `export` line to your shell's configuration file (e.g., `~/.zshrc`). +Notes: + +- This command only sets the key for your current terminal session, which we recommend. To set it for all future sessions, you can also add the `export` line to your shell's configuration file (e.g., `~/.zshrc`). +- If you have signed in with ChatGPT, Codex will default to using your ChatGPT credits. If you wish to use your API key, use the `/logout` command to clear your ChatGPT authentication. ### Choosing Codex's level of autonomy From 408c7ca142689136d887676def1bf41ea80bb2a9 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 8 Aug 2025 16:09:39 -0700 Subject: [PATCH 0113/1309] chore: remove the TypeScript code from the repository (#2048) This deletes the bulk of the `codex-cli` folder and eliminates the logic that builds the TypeScript code and bundles it into the release. Since this PR modifies `.github/workflows/rust-release.yml`, to test changes to the release process, I locally commented out all of the "is this commit on upstream `main`" checks in `scripts/create_github_release.sh` and ran: ``` ./codex-rs/scripts/create_github_release.sh 0.20.0-alpha.4 ``` Which kicked off: https://github.com/openai/codex/actions/runs/16842085113 And the release artifacts appear legit! https://github.com/openai/codex/releases/tag/rust-v0.20.0-alpha.4 --- .github/workflows/ci.yml | 27 +- .github/workflows/codex.yml | 31 - .github/workflows/rust-release.yml | 26 - .husky/pre-commit | 1 - codex-cli/.editorconfig | 9 - codex-cli/.eslintrc.cjs | 107 - codex-cli/HUSKY.md | 45 - codex-cli/build.mjs | 88 - codex-cli/default.nix | 43 - codex-cli/examples/README.md | 44 - codex-cli/examples/build-codex-demo/run.sh | 65 - .../examples/build-codex-demo/runs/.gitkeep | 0 codex-cli/examples/build-codex-demo/task.yaml | 88 - codex-cli/examples/camerascii/run.sh | 68 - codex-cli/examples/camerascii/runs/.gitkeep | 0 codex-cli/examples/camerascii/task.yaml | 5 - .../camerascii/template/screenshot_details.md | 34 - codex-cli/examples/impossible-pong/run.sh | 68 - .../examples/impossible-pong/runs/.gitkeep | 0 codex-cli/examples/impossible-pong/task.yaml | 11 - .../impossible-pong/template/index.html | 233 - codex-cli/examples/prompt-analyzer/run.sh | 68 - .../examples/prompt-analyzer/runs/.gitkeep | 0 codex-cli/examples/prompt-analyzer/task.yaml | 17 - .../prompt-analyzer/template/Clustering.ipynb | 231 - .../prompt-analyzer/template/README.md | 103 - .../prompt-analyzer/template/analysis.md | 23 - .../template/analysis_dbscan.md | 22 - .../template/cluster_prompts.py | 547 -- .../template/plots/cluster_sizes.png | Bin 19000 -> 0 bytes .../prompt-analyzer/template/plots/tsne.png | Bin 102093 -> 0 bytes .../template/plots_dbscan/cluster_sizes.png | Bin 20441 -> 0 bytes .../template/plots_dbscan/tsne.png | Bin 96389 -> 0 bytes .../prompt-analyzer/template/prompts.csv | 214 - codex-cli/examples/prompting_guide.md | 117 - codex-cli/ignore-react-devtools-plugin.js | 16 - codex-cli/package.json | 71 +- codex-cli/require-shim.js | 11 - codex-cli/scripts/stage_release.sh | 8 - codex-cli/src/app.tsx | 108 - codex-cli/src/approvals.ts | 633 -- codex-cli/src/cli-singlepass.tsx | 28 - codex-cli/src/cli.tsx | 740 --- .../src/components/approval-mode-overlay.tsx | 47 - .../src/components/chat/message-history.tsx | 86 - .../src/components/chat/multiline-editor.tsx | 392 -- .../chat/terminal-chat-command-review.tsx | 256 - .../chat/terminal-chat-completions.tsx | 64 - .../chat/terminal-chat-input-thinking.tsx | 129 - .../components/chat/terminal-chat-input.tsx | 1017 ---- .../chat/terminal-chat-past-rollout.tsx | 68 - .../chat/terminal-chat-response-item.tsx | 360 -- .../chat/terminal-chat-tool-call-command.tsx | 143 - .../src/components/chat/terminal-chat.tsx | 766 --- .../src/components/chat/terminal-header.tsx | 99 - .../chat/terminal-message-history.tsx | 93 - .../components/chat/use-message-grouping.ts | 9 - codex-cli/src/components/diff-overlay.tsx | 93 - codex-cli/src/components/help-overlay.tsx | 103 - codex-cli/src/components/history-overlay.tsx | 255 - codex-cli/src/components/model-overlay.tsx | 165 - .../onboarding/onboarding-approval-mode.tsx | 35 - .../src/components/select-input/indicator.tsx | 21 - .../src/components/select-input/item.tsx | 13 - .../components/select-input/select-input.tsx | 189 - codex-cli/src/components/sessions-overlay.tsx | 130 - .../src/components/singlepass-cli-app.tsx | 677 --- .../src/components/typeahead-overlay.tsx | 166 - .../components/vendor/cli-spinners/index.js | 1293 ---- .../src/components/vendor/ink-select/index.js | 1 - .../vendor/ink-select/option-map.js | 26 - .../vendor/ink-select/select-option.js | 27 - .../components/vendor/ink-select/select.js | 53 - .../src/components/vendor/ink-select/theme.js | 32 - .../vendor/ink-select/use-select-state.js | 158 - .../vendor/ink-select/use-select.js | 17 - .../src/components/vendor/ink-spinner.tsx | 36 - .../src/components/vendor/ink-text-input.tsx | 428 -- codex-cli/src/format-command.ts | 53 - codex-cli/src/hooks/use-confirmation.ts | 67 - codex-cli/src/hooks/use-terminal-size.ts | 26 - codex-cli/src/parse-apply-patch.ts | 113 - codex-cli/src/shims-external.d.ts | 24 - codex-cli/src/text-buffer.ts | 977 --- codex-cli/src/typings.d.ts | 65 - codex-cli/src/utils/agent/agent-loop.ts | 1674 ----- codex-cli/src/utils/agent/apply-patch.ts | 815 --- codex-cli/src/utils/agent/exec.ts | 138 - .../src/utils/agent/handle-exec-command.ts | 377 -- .../src/utils/agent/parse-apply-patch.ts | 112 - .../src/utils/agent/platform-commands.ts | 82 - codex-cli/src/utils/agent/review.ts | 14 - .../sandbox/create-truncating-collector.ts | 77 - .../src/utils/agent/sandbox/interface.ts | 30 - codex-cli/src/utils/agent/sandbox/landlock.ts | 175 - .../src/utils/agent/sandbox/macos-seatbelt.ts | 154 - codex-cli/src/utils/agent/sandbox/raw-exec.ts | 238 - .../src/utils/approximate-tokens-used.ts | 55 - codex-cli/src/utils/auto-approval-mode.js | 9 - codex-cli/src/utils/auto-approval-mode.ts | 10 - codex-cli/src/utils/bug-report.ts | 82 - codex-cli/src/utils/check-in-git.ts | 31 - codex-cli/src/utils/check-updates.ts | 146 - codex-cli/src/utils/compact-summary.ts | 70 - codex-cli/src/utils/config.ts | 597 -- .../src/utils/extract-applied-patches.ts | 36 - .../src/utils/file-system-suggestions.ts | 59 - codex-cli/src/utils/file-tag-utils.ts | 62 - .../src/utils/get-api-key-components.tsx | 75 - codex-cli/src/utils/get-api-key.tsx | 766 --- codex-cli/src/utils/get-diff.ts | 129 - codex-cli/src/utils/input-utils.ts | 39 - codex-cli/src/utils/logger/log.ts | 137 - codex-cli/src/utils/model-info.ts | 202 - codex-cli/src/utils/model-utils.ts | 196 - codex-cli/src/utils/openai-client.ts | 51 - .../src/utils/package-manager-detector.ts | 73 - codex-cli/src/utils/parsers.ts | 113 - codex-cli/src/utils/providers.ts | 55 - codex-cli/src/utils/responses.ts | 717 --- codex-cli/src/utils/session.ts | 52 - codex-cli/src/utils/short-path.ts | 27 - codex-cli/src/utils/singlepass/code_diff.ts | 190 - codex-cli/src/utils/singlepass/context.ts | 65 - .../src/utils/singlepass/context_files.ts | 409 -- .../src/utils/singlepass/context_limit.ts | 208 - codex-cli/src/utils/singlepass/file_ops.ts | 47 - codex-cli/src/utils/slash-commands.ts | 36 - .../src/utils/storage/command-history.ts | 139 - codex-cli/src/utils/storage/save-rollout.ts | 52 - codex-cli/src/utils/terminal-chat-utils.ts | 0 codex-cli/src/utils/terminal.ts | 84 - codex-cli/src/version.ts | 8 - codex-cli/tests/__fixtures__/a.txt | 1 - codex-cli/tests/__fixtures__/b.txt | 1 - .../__snapshots__/check-updates.test.ts.snap | 12 - .../agent-azure-responses-endpoint.test.ts | 107 - codex-cli/tests/agent-cancel-early.test.ts | 128 - .../tests/agent-cancel-prev-response.test.ts | 149 - codex-cli/tests/agent-cancel-race.test.ts | 137 - codex-cli/tests/agent-cancel.test.ts | 171 - codex-cli/tests/agent-dedupe-items.test.ts | 115 - .../tests/agent-function-call-id.test.ts | 150 - .../tests/agent-generic-network-error.test.ts | 134 - .../tests/agent-interrupt-continue.test.ts | 148 - .../tests/agent-invalid-request-error.test.ts | 89 - .../tests/agent-max-tokens-error.test.ts | 93 - codex-cli/tests/agent-network-errors.test.ts | 181 - codex-cli/tests/agent-project-doc.test.ts | 142 - .../tests/agent-rate-limit-error.test.ts | 128 - codex-cli/tests/agent-server-retry.test.ts | 168 - codex-cli/tests/agent-terminate.test.ts | 180 - codex-cli/tests/agent-thinking-time.test.ts | 174 - codex-cli/tests/api-key.test.ts | 35 - codex-cli/tests/apply-patch.test.ts | 346 -- codex-cli/tests/approvals.test.ts | 219 - codex-cli/tests/cancel-exec.test.ts | 56 - codex-cli/tests/check-updates.test.ts | 178 - codex-cli/tests/clear-command.test.tsx | 120 - codex-cli/tests/config.test.tsx | 363 -- codex-cli/tests/config_reasoning.test.ts | 121 - .../tests/create-truncating-collector.test.ts | 55 - .../disableResponseStorage.agentLoop.test.ts | 93 - .../tests/disableResponseStorage.test.ts | 46 - codex-cli/tests/dummy.test.ts | 4 - codex-cli/tests/exec-apply-patch.test.ts | 44 - .../tests/file-system-suggestions.test.ts | 92 - codex-cli/tests/file-tag-utils.test.ts | 240 - codex-cli/tests/fixed-requires-shell.test.ts | 38 - codex-cli/tests/format-command.test.ts | 21 - .../tests/get-diff-special-chars.test.ts | 28 - codex-cli/tests/history-overlay.test.tsx | 350 -- codex-cli/tests/input-utils.test.ts | 47 - .../tests/invalid-command-handling.test.ts | 68 - codex-cli/tests/markdown.test.tsx | 172 - codex-cli/tests/model-info.test.ts | 19 - .../tests/model-utils-network-error.test.ts | 76 - codex-cli/tests/model-utils.test.ts | 78 - .../multiline-ctrl-enter-submit.test.tsx | 41 - .../tests/multiline-dynamic-width.test.tsx | 77 - .../tests/multiline-enter-submit-cr.test.tsx | 41 - .../tests/multiline-history-behavior.test.tsx | 180 - codex-cli/tests/multiline-input-test.ts | 164 - codex-cli/tests/multiline-newline.test.tsx | 56 - .../tests/multiline-shift-enter-crlf.test.tsx | 51 - .../tests/multiline-shift-enter-mod1.test.tsx | 49 - .../tests/multiline-shift-enter.test.tsx | 49 - .../tests/package-manager-detector.test.ts | 66 - codex-cli/tests/parse-apply-patch.test.ts | 45 - codex-cli/tests/pipe-command.test.ts | 19 - codex-cli/tests/project-doc.test.ts | 57 - .../tests/raw-exec-process-group.test.ts | 87 - codex-cli/tests/requires-shell.test.ts | 49 - .../tests/responses-chat-completions.test.ts | 815 --- codex-cli/tests/slash-commands.test.ts | 40 - .../tests/terminal-chat-completions.test.tsx | 46 - .../terminal-chat-input-compact.test.tsx | 34 - ...l-chat-input-file-tag-suggestions.test.tsx | 207 - .../terminal-chat-input-multiline.test.tsx | 130 - .../terminal-chat-model-selection.test.tsx | 130 - .../terminal-chat-response-item.test.tsx | 65 - .../tests/text-buffer-copy-paste.test.ts | 50 - codex-cli/tests/text-buffer-crlf.test.ts | 14 - codex-cli/tests/text-buffer-gaps.test.ts | 250 - codex-cli/tests/text-buffer-word.test.ts | 137 - codex-cli/tests/text-buffer.test.ts | 291 - .../tests/token-streaming-performance.test.ts | 110 - codex-cli/tests/typeahead-scroll.test.tsx | 68 - codex-cli/tests/ui-test-helpers.tsx | 28 - codex-cli/tests/user-config-env.test.ts | 62 - codex-cli/tsconfig.json | 34 - codex-cli/vitest.config.ts | 12 - package.json | 29 +- patches/marked-terminal@7.3.0.patch | 26 - pnpm-lock.yaml | 5397 ----------------- pnpm-workspace.yaml | 5 - 216 files changed, 3 insertions(+), 35960 deletions(-) delete mode 100644 .husky/pre-commit delete mode 100644 codex-cli/.editorconfig delete mode 100644 codex-cli/.eslintrc.cjs delete mode 100644 codex-cli/HUSKY.md delete mode 100644 codex-cli/build.mjs delete mode 100644 codex-cli/default.nix delete mode 100644 codex-cli/examples/README.md delete mode 100755 codex-cli/examples/build-codex-demo/run.sh delete mode 100644 codex-cli/examples/build-codex-demo/runs/.gitkeep delete mode 100644 codex-cli/examples/build-codex-demo/task.yaml delete mode 100755 codex-cli/examples/camerascii/run.sh delete mode 100644 codex-cli/examples/camerascii/runs/.gitkeep delete mode 100644 codex-cli/examples/camerascii/task.yaml delete mode 100644 codex-cli/examples/camerascii/template/screenshot_details.md delete mode 100755 codex-cli/examples/impossible-pong/run.sh delete mode 100644 codex-cli/examples/impossible-pong/runs/.gitkeep delete mode 100644 codex-cli/examples/impossible-pong/task.yaml delete mode 100644 codex-cli/examples/impossible-pong/template/index.html delete mode 100755 codex-cli/examples/prompt-analyzer/run.sh delete mode 100644 codex-cli/examples/prompt-analyzer/runs/.gitkeep delete mode 100644 codex-cli/examples/prompt-analyzer/task.yaml delete mode 100644 codex-cli/examples/prompt-analyzer/template/Clustering.ipynb delete mode 100644 codex-cli/examples/prompt-analyzer/template/README.md delete mode 100644 codex-cli/examples/prompt-analyzer/template/analysis.md delete mode 100644 codex-cli/examples/prompt-analyzer/template/analysis_dbscan.md delete mode 100644 codex-cli/examples/prompt-analyzer/template/cluster_prompts.py delete mode 100644 codex-cli/examples/prompt-analyzer/template/plots/cluster_sizes.png delete mode 100644 codex-cli/examples/prompt-analyzer/template/plots/tsne.png delete mode 100644 codex-cli/examples/prompt-analyzer/template/plots_dbscan/cluster_sizes.png delete mode 100644 codex-cli/examples/prompt-analyzer/template/plots_dbscan/tsne.png delete mode 100644 codex-cli/examples/prompt-analyzer/template/prompts.csv delete mode 100644 codex-cli/examples/prompting_guide.md delete mode 100644 codex-cli/ignore-react-devtools-plugin.js delete mode 100644 codex-cli/require-shim.js delete mode 100644 codex-cli/src/app.tsx delete mode 100644 codex-cli/src/approvals.ts delete mode 100644 codex-cli/src/cli-singlepass.tsx delete mode 100644 codex-cli/src/cli.tsx delete mode 100644 codex-cli/src/components/approval-mode-overlay.tsx delete mode 100644 codex-cli/src/components/chat/message-history.tsx delete mode 100644 codex-cli/src/components/chat/multiline-editor.tsx delete mode 100644 codex-cli/src/components/chat/terminal-chat-command-review.tsx delete mode 100644 codex-cli/src/components/chat/terminal-chat-completions.tsx delete mode 100644 codex-cli/src/components/chat/terminal-chat-input-thinking.tsx delete mode 100644 codex-cli/src/components/chat/terminal-chat-input.tsx delete mode 100644 codex-cli/src/components/chat/terminal-chat-past-rollout.tsx delete mode 100644 codex-cli/src/components/chat/terminal-chat-response-item.tsx delete mode 100644 codex-cli/src/components/chat/terminal-chat-tool-call-command.tsx delete mode 100644 codex-cli/src/components/chat/terminal-chat.tsx delete mode 100644 codex-cli/src/components/chat/terminal-header.tsx delete mode 100644 codex-cli/src/components/chat/terminal-message-history.tsx delete mode 100644 codex-cli/src/components/chat/use-message-grouping.ts delete mode 100644 codex-cli/src/components/diff-overlay.tsx delete mode 100644 codex-cli/src/components/help-overlay.tsx delete mode 100644 codex-cli/src/components/history-overlay.tsx delete mode 100644 codex-cli/src/components/model-overlay.tsx delete mode 100644 codex-cli/src/components/onboarding/onboarding-approval-mode.tsx delete mode 100644 codex-cli/src/components/select-input/indicator.tsx delete mode 100644 codex-cli/src/components/select-input/item.tsx delete mode 100644 codex-cli/src/components/select-input/select-input.tsx delete mode 100644 codex-cli/src/components/sessions-overlay.tsx delete mode 100644 codex-cli/src/components/singlepass-cli-app.tsx delete mode 100644 codex-cli/src/components/typeahead-overlay.tsx delete mode 100644 codex-cli/src/components/vendor/cli-spinners/index.js delete mode 100644 codex-cli/src/components/vendor/ink-select/index.js delete mode 100644 codex-cli/src/components/vendor/ink-select/option-map.js delete mode 100644 codex-cli/src/components/vendor/ink-select/select-option.js delete mode 100644 codex-cli/src/components/vendor/ink-select/select.js delete mode 100644 codex-cli/src/components/vendor/ink-select/theme.js delete mode 100644 codex-cli/src/components/vendor/ink-select/use-select-state.js delete mode 100644 codex-cli/src/components/vendor/ink-select/use-select.js delete mode 100644 codex-cli/src/components/vendor/ink-spinner.tsx delete mode 100644 codex-cli/src/components/vendor/ink-text-input.tsx delete mode 100644 codex-cli/src/format-command.ts delete mode 100644 codex-cli/src/hooks/use-confirmation.ts delete mode 100644 codex-cli/src/hooks/use-terminal-size.ts delete mode 100644 codex-cli/src/parse-apply-patch.ts delete mode 100644 codex-cli/src/shims-external.d.ts delete mode 100644 codex-cli/src/text-buffer.ts delete mode 100644 codex-cli/src/typings.d.ts delete mode 100644 codex-cli/src/utils/agent/agent-loop.ts delete mode 100644 codex-cli/src/utils/agent/apply-patch.ts delete mode 100644 codex-cli/src/utils/agent/exec.ts delete mode 100644 codex-cli/src/utils/agent/handle-exec-command.ts delete mode 100644 codex-cli/src/utils/agent/parse-apply-patch.ts delete mode 100644 codex-cli/src/utils/agent/platform-commands.ts delete mode 100644 codex-cli/src/utils/agent/review.ts delete mode 100644 codex-cli/src/utils/agent/sandbox/create-truncating-collector.ts delete mode 100644 codex-cli/src/utils/agent/sandbox/interface.ts delete mode 100644 codex-cli/src/utils/agent/sandbox/landlock.ts delete mode 100644 codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts delete mode 100644 codex-cli/src/utils/agent/sandbox/raw-exec.ts delete mode 100644 codex-cli/src/utils/approximate-tokens-used.ts delete mode 100644 codex-cli/src/utils/auto-approval-mode.js delete mode 100644 codex-cli/src/utils/auto-approval-mode.ts delete mode 100644 codex-cli/src/utils/bug-report.ts delete mode 100644 codex-cli/src/utils/check-in-git.ts delete mode 100644 codex-cli/src/utils/check-updates.ts delete mode 100644 codex-cli/src/utils/compact-summary.ts delete mode 100644 codex-cli/src/utils/config.ts delete mode 100644 codex-cli/src/utils/extract-applied-patches.ts delete mode 100644 codex-cli/src/utils/file-system-suggestions.ts delete mode 100644 codex-cli/src/utils/file-tag-utils.ts delete mode 100644 codex-cli/src/utils/get-api-key-components.tsx delete mode 100644 codex-cli/src/utils/get-api-key.tsx delete mode 100644 codex-cli/src/utils/get-diff.ts delete mode 100644 codex-cli/src/utils/input-utils.ts delete mode 100644 codex-cli/src/utils/logger/log.ts delete mode 100644 codex-cli/src/utils/model-info.ts delete mode 100644 codex-cli/src/utils/model-utils.ts delete mode 100644 codex-cli/src/utils/openai-client.ts delete mode 100644 codex-cli/src/utils/package-manager-detector.ts delete mode 100644 codex-cli/src/utils/parsers.ts delete mode 100644 codex-cli/src/utils/providers.ts delete mode 100644 codex-cli/src/utils/responses.ts delete mode 100644 codex-cli/src/utils/session.ts delete mode 100644 codex-cli/src/utils/short-path.ts delete mode 100644 codex-cli/src/utils/singlepass/code_diff.ts delete mode 100644 codex-cli/src/utils/singlepass/context.ts delete mode 100644 codex-cli/src/utils/singlepass/context_files.ts delete mode 100644 codex-cli/src/utils/singlepass/context_limit.ts delete mode 100644 codex-cli/src/utils/singlepass/file_ops.ts delete mode 100644 codex-cli/src/utils/slash-commands.ts delete mode 100644 codex-cli/src/utils/storage/command-history.ts delete mode 100644 codex-cli/src/utils/storage/save-rollout.ts delete mode 100644 codex-cli/src/utils/terminal-chat-utils.ts delete mode 100644 codex-cli/src/utils/terminal.ts delete mode 100644 codex-cli/src/version.ts delete mode 100644 codex-cli/tests/__fixtures__/a.txt delete mode 100644 codex-cli/tests/__fixtures__/b.txt delete mode 100644 codex-cli/tests/__snapshots__/check-updates.test.ts.snap delete mode 100644 codex-cli/tests/agent-azure-responses-endpoint.test.ts delete mode 100644 codex-cli/tests/agent-cancel-early.test.ts delete mode 100644 codex-cli/tests/agent-cancel-prev-response.test.ts delete mode 100644 codex-cli/tests/agent-cancel-race.test.ts delete mode 100644 codex-cli/tests/agent-cancel.test.ts delete mode 100644 codex-cli/tests/agent-dedupe-items.test.ts delete mode 100644 codex-cli/tests/agent-function-call-id.test.ts delete mode 100644 codex-cli/tests/agent-generic-network-error.test.ts delete mode 100644 codex-cli/tests/agent-interrupt-continue.test.ts delete mode 100644 codex-cli/tests/agent-invalid-request-error.test.ts delete mode 100644 codex-cli/tests/agent-max-tokens-error.test.ts delete mode 100644 codex-cli/tests/agent-network-errors.test.ts delete mode 100644 codex-cli/tests/agent-project-doc.test.ts delete mode 100644 codex-cli/tests/agent-rate-limit-error.test.ts delete mode 100644 codex-cli/tests/agent-server-retry.test.ts delete mode 100644 codex-cli/tests/agent-terminate.test.ts delete mode 100644 codex-cli/tests/agent-thinking-time.test.ts delete mode 100644 codex-cli/tests/api-key.test.ts delete mode 100644 codex-cli/tests/apply-patch.test.ts delete mode 100644 codex-cli/tests/approvals.test.ts delete mode 100644 codex-cli/tests/cancel-exec.test.ts delete mode 100644 codex-cli/tests/check-updates.test.ts delete mode 100644 codex-cli/tests/clear-command.test.tsx delete mode 100644 codex-cli/tests/config.test.tsx delete mode 100644 codex-cli/tests/config_reasoning.test.ts delete mode 100644 codex-cli/tests/create-truncating-collector.test.ts delete mode 100644 codex-cli/tests/disableResponseStorage.agentLoop.test.ts delete mode 100644 codex-cli/tests/disableResponseStorage.test.ts delete mode 100644 codex-cli/tests/dummy.test.ts delete mode 100644 codex-cli/tests/exec-apply-patch.test.ts delete mode 100644 codex-cli/tests/file-system-suggestions.test.ts delete mode 100644 codex-cli/tests/file-tag-utils.test.ts delete mode 100644 codex-cli/tests/fixed-requires-shell.test.ts delete mode 100644 codex-cli/tests/format-command.test.ts delete mode 100644 codex-cli/tests/get-diff-special-chars.test.ts delete mode 100644 codex-cli/tests/history-overlay.test.tsx delete mode 100644 codex-cli/tests/input-utils.test.ts delete mode 100644 codex-cli/tests/invalid-command-handling.test.ts delete mode 100644 codex-cli/tests/markdown.test.tsx delete mode 100644 codex-cli/tests/model-info.test.ts delete mode 100644 codex-cli/tests/model-utils-network-error.test.ts delete mode 100644 codex-cli/tests/model-utils.test.ts delete mode 100644 codex-cli/tests/multiline-ctrl-enter-submit.test.tsx delete mode 100644 codex-cli/tests/multiline-dynamic-width.test.tsx delete mode 100644 codex-cli/tests/multiline-enter-submit-cr.test.tsx delete mode 100644 codex-cli/tests/multiline-history-behavior.test.tsx delete mode 100644 codex-cli/tests/multiline-input-test.ts delete mode 100644 codex-cli/tests/multiline-newline.test.tsx delete mode 100644 codex-cli/tests/multiline-shift-enter-crlf.test.tsx delete mode 100644 codex-cli/tests/multiline-shift-enter-mod1.test.tsx delete mode 100644 codex-cli/tests/multiline-shift-enter.test.tsx delete mode 100644 codex-cli/tests/package-manager-detector.test.ts delete mode 100644 codex-cli/tests/parse-apply-patch.test.ts delete mode 100644 codex-cli/tests/pipe-command.test.ts delete mode 100644 codex-cli/tests/project-doc.test.ts delete mode 100644 codex-cli/tests/raw-exec-process-group.test.ts delete mode 100644 codex-cli/tests/requires-shell.test.ts delete mode 100644 codex-cli/tests/responses-chat-completions.test.ts delete mode 100644 codex-cli/tests/slash-commands.test.ts delete mode 100644 codex-cli/tests/terminal-chat-completions.test.tsx delete mode 100644 codex-cli/tests/terminal-chat-input-compact.test.tsx delete mode 100644 codex-cli/tests/terminal-chat-input-file-tag-suggestions.test.tsx delete mode 100644 codex-cli/tests/terminal-chat-input-multiline.test.tsx delete mode 100644 codex-cli/tests/terminal-chat-model-selection.test.tsx delete mode 100644 codex-cli/tests/terminal-chat-response-item.test.tsx delete mode 100644 codex-cli/tests/text-buffer-copy-paste.test.ts delete mode 100644 codex-cli/tests/text-buffer-crlf.test.ts delete mode 100644 codex-cli/tests/text-buffer-gaps.test.ts delete mode 100644 codex-cli/tests/text-buffer-word.test.ts delete mode 100644 codex-cli/tests/text-buffer.test.ts delete mode 100644 codex-cli/tests/token-streaming-performance.test.ts delete mode 100644 codex-cli/tests/typeahead-scroll.test.tsx delete mode 100644 codex-cli/tests/ui-test-helpers.tsx delete mode 100644 codex-cli/tests/user-config-env.test.ts delete mode 100644 codex-cli/tsconfig.json delete mode 100644 codex-cli/vitest.config.ts delete mode 100644 patches/marked-terminal@7.3.0.patch diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d8675fa5f..32fb070924 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,35 +44,10 @@ jobs: # Run all tasks using workspace filters - - name: Check TypeScript code formatting - working-directory: codex-cli - run: pnpm run format - - - name: Check Markdown and config file formatting - run: pnpm run format - - - name: Run tests - run: pnpm run test - - - name: Lint - run: | - pnpm --filter @openai/codex exec -- eslint src tests --ext ts --ext tsx \ - --report-unused-disable-directives \ - --rule "no-console:error" \ - --rule "no-debugger:error" \ - --max-warnings=-1 - - - name: Type-check - run: pnpm run typecheck - - - name: Build - run: pnpm run build - - name: Ensure staging a release works. - working-directory: codex-cli env: GH_TOKEN: ${{ github.token }} - run: pnpm stage-release + run: ./codex-cli/scripts/stage_release.sh - name: Ensure root README.md contains only ASCII and certain Unicode code points run: ./scripts/asciicheck.py README.md diff --git a/.github/workflows/codex.yml b/.github/workflows/codex.yml index 18fe74cc85..6ca0d57f46 100644 --- a/.github/workflows/codex.yml +++ b/.github/workflows/codex.yml @@ -39,37 +39,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - # We install the dependencies like we would for an ordinary CI job, - # particularly because Codex will not have network access to install - # these dependencies. - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 10.8.1 - run_install: false - - - name: Get pnpm store directory - id: pnpm-cache - shell: bash - run: | - echo "store_path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT - - - name: Setup pnpm cache - uses: actions/cache@v4 - with: - path: ${{ steps.pnpm-cache.outputs.store_path }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Install dependencies - run: pnpm install - - uses: dtolnay/rust-toolchain@1.88 with: targets: x86_64-unknown-linux-gnu diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index fc0938e20b..8e5aef2757 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -172,32 +172,6 @@ jobs: version="${GITHUB_REF_NAME#rust-v}" echo "name=${version}" >> $GITHUB_OUTPUT - # Setup Node + pnpm similar to ci.yml so we can build the npm package - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 10.8.1 - run_install: false - - - name: Get pnpm store directory - id: pnpm-cache - shell: bash - run: | - echo "store_path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT - - - name: Setup pnpm cache - uses: actions/cache@v4 - with: - path: ${{ steps.pnpm-cache.outputs.store_path }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - name: Stage npm package env: GH_TOKEN: ${{ github.token }} diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100644 index e02c24e2b5..0000000000 --- a/.husky/pre-commit +++ /dev/null @@ -1 +0,0 @@ -pnpm lint-staged \ No newline at end of file diff --git a/codex-cli/.editorconfig b/codex-cli/.editorconfig deleted file mode 100644 index f55d420563..0000000000 --- a/codex-cli/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -root = true - -[*] -indent_style = space -indent_size = 2 - -[*.{js,ts,jsx,tsx}] -indent_style = space -indent_size = 2 \ No newline at end of file diff --git a/codex-cli/.eslintrc.cjs b/codex-cli/.eslintrc.cjs deleted file mode 100644 index a623d2edb0..0000000000 --- a/codex-cli/.eslintrc.cjs +++ /dev/null @@ -1,107 +0,0 @@ -module.exports = { - root: true, - env: { browser: true, node: true, es2020: true }, - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react-hooks/recommended", - ], - ignorePatterns: [ - ".eslintrc.cjs", - "build.mjs", - "dist", - "vite.config.ts", - "src/components/vendor", - ], - parser: "@typescript-eslint/parser", - parserOptions: { - tsconfigRootDir: __dirname, - project: ["./tsconfig.json"], - }, - plugins: ["import", "react-hooks", "react-refresh"], - rules: { - // Imports - "@typescript-eslint/consistent-type-imports": "error", - "import/no-cycle": ["error", { maxDepth: 1 }], - "import/no-duplicates": "error", - "import/order": [ - "error", - { - groups: ["type"], - "newlines-between": "always", - alphabetize: { - order: "asc", - caseInsensitive: false, - }, - }, - ], - // We use the import/ plugin instead. - "sort-imports": "off", - - "@typescript-eslint/array-type": ["error", { default: "generic" }], - // FIXME(mbolin): Introduce this. - // "@typescript-eslint/explicit-function-return-type": "error", - "@typescript-eslint/explicit-module-boundary-types": "error", - "@typescript-eslint/no-explicit-any": "error", - "@typescript-eslint/switch-exhaustiveness-check": [ - "error", - { - allowDefaultCaseForExhaustiveSwitch: false, - requireDefaultForNonUnion: true, - }, - ], - - // Use typescript-eslint/no-unused-vars, no-unused-vars reports - // false positives with typescript - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": [ - "error", - { - argsIgnorePattern: "^_", - varsIgnorePattern: "^_", - caughtErrorsIgnorePattern: "^_", - }, - ], - - curly: "error", - - eqeqeq: ["error", "always", { null: "never" }], - "react-refresh/only-export-components": [ - "error", - { allowConstantExport: true }, - ], - "no-await-in-loop": "error", - "no-bitwise": "error", - "no-caller": "error", - // This is fine during development, but should not be checked in. - "no-console": "error", - // This is fine during development, but should not be checked in. - "no-debugger": "error", - "no-duplicate-case": "error", - "no-eval": "error", - "no-ex-assign": "error", - "no-return-await": "error", - "no-param-reassign": "error", - "no-script-url": "error", - "no-self-compare": "error", - "no-unsafe-finally": "error", - "no-var": "error", - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "error", - }, - overrides: [ - { - // apply only to files under tests/ - files: ["tests/**/*.{ts,tsx,js,jsx}"], - rules: { - "@typescript-eslint/no-explicit-any": "off", - "import/order": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/no-var-requires": "off", - "no-await-in-loop": "off", - "no-control-regex": "off", - }, - }, - ], -}; diff --git a/codex-cli/HUSKY.md b/codex-cli/HUSKY.md deleted file mode 100644 index d525e2f743..0000000000 --- a/codex-cli/HUSKY.md +++ /dev/null @@ -1,45 +0,0 @@ -# Husky Git Hooks - -This project uses [Husky](https://typicode.github.io/husky/) to enforce code quality checks before commits and pushes. - -## What's Included - -- **Pre-commit Hook**: Runs lint-staged to check files that are about to be committed. - - - Lints and formats TypeScript/TSX files using ESLint and Prettier - - Formats JSON, MD, and YML files using Prettier - -- **Pre-push Hook**: Runs tests and type checking before pushing to the remote repository. - - Executes `npm test` to run all tests - - Executes `npm run typecheck` to check TypeScript types - -## Benefits - -- Ensures consistent code style across the project -- Prevents pushing code with failing tests or type errors -- Reduces the need for style-related code review comments -- Improves overall code quality - -## For Contributors - -You don't need to do anything special to use these hooks. They will automatically run when you commit or push code. - -If you need to bypass the hooks in exceptional cases: - -```bash -# Skip pre-commit hooks -git commit -m "Your message" --no-verify - -# Skip pre-push hooks -git push --no-verify -``` - -Note: Please use these bypass options sparingly and only when absolutely necessary. - -## Troubleshooting - -If you encounter any issues with the hooks: - -1. Make sure you have the latest dependencies installed: `npm install` -2. Ensure the hook scripts are executable (Unix systems): `chmod +x .husky/pre-commit .husky/pre-push` -3. Check if there are any ESLint or Prettier configuration issues in your code diff --git a/codex-cli/build.mjs b/codex-cli/build.mjs deleted file mode 100644 index 16664d76fc..0000000000 --- a/codex-cli/build.mjs +++ /dev/null @@ -1,88 +0,0 @@ -import * as esbuild from "esbuild"; -import * as fs from "fs"; -import * as path from "path"; - -const OUT_DIR = 'dist' -/** - * ink attempts to import react-devtools-core in an ESM-unfriendly way: - * - * https://github.com/vadimdemedes/ink/blob/eab6ef07d4030606530d58d3d7be8079b4fb93bb/src/reconciler.ts#L22-L45 - * - * to make this work, we have to strip the import out of the build. - */ -const ignoreReactDevToolsPlugin = { - name: "ignore-react-devtools", - setup(build) { - // When an import for 'react-devtools-core' is encountered, - // return an empty module. - build.onResolve({ filter: /^react-devtools-core$/ }, (args) => { - return { path: args.path, namespace: "ignore-devtools" }; - }); - build.onLoad({ filter: /.*/, namespace: "ignore-devtools" }, () => { - return { contents: "", loader: "js" }; - }); - }, -}; - -// ---------------------------------------------------------------------------- -// Build mode detection (production vs development) -// -// • production (default): minified, external telemetry shebang handling. -// • development (--dev|NODE_ENV=development|CODEX_DEV=1): -// – no minification -// – inline source maps for better stacktraces -// – shebang tweaked to enable Node's source‑map support at runtime -// ---------------------------------------------------------------------------- - -const isDevBuild = - process.argv.includes("--dev") || - process.env.CODEX_DEV === "1" || - process.env.NODE_ENV === "development"; - -const plugins = [ignoreReactDevToolsPlugin]; - -// Build Hygiene, ensure we drop previous dist dir and any leftover files -const outPath = path.resolve(OUT_DIR); -if (fs.existsSync(outPath)) { - fs.rmSync(outPath, { recursive: true, force: true }); -} - -// Add a shebang that enables source‑map support for dev builds so that stack -// traces point to the original TypeScript lines without requiring callers to -// remember to set NODE_OPTIONS manually. -if (isDevBuild) { - const devShebangLine = - "#!/usr/bin/env -S NODE_OPTIONS=--enable-source-maps node\n"; - const devShebangPlugin = { - name: "dev-shebang", - setup(build) { - build.onEnd(async () => { - const outFile = path.resolve(isDevBuild ? `${OUT_DIR}/cli-dev.js` : `${OUT_DIR}/cli.js`); - let code = await fs.promises.readFile(outFile, "utf8"); - if (code.startsWith("#!")) { - code = code.replace(/^#!.*\n/, devShebangLine); - await fs.promises.writeFile(outFile, code, "utf8"); - } - }); - }, - }; - plugins.push(devShebangPlugin); -} - -esbuild - .build({ - entryPoints: ["src/cli.tsx"], - // Do not bundle the contents of package.json at build time: always read it - // at runtime. - external: ["../package.json"], - bundle: true, - format: "esm", - platform: "node", - tsconfig: "tsconfig.json", - outfile: isDevBuild ? `${OUT_DIR}/cli-dev.js` : `${OUT_DIR}/cli.js`, - minify: !isDevBuild, - sourcemap: isDevBuild ? "inline" : true, - plugins, - inject: ["./require-shim.js"], - }) - .catch(() => process.exit(1)); diff --git a/codex-cli/default.nix b/codex-cli/default.nix deleted file mode 100644 index 6ae19bb73a..0000000000 --- a/codex-cli/default.nix +++ /dev/null @@ -1,43 +0,0 @@ -{ pkgs, monorep-deps ? [], ... }: -let - node = pkgs.nodejs_22; -in -rec { - package = pkgs.buildNpmPackage { - pname = "codex-cli"; - version = "0.1.0"; - src = ./.; - npmDepsHash = "sha256-3tAalmh50I0fhhd7XreM+jvl0n4zcRhqygFNB1Olst8"; - nodejs = node; - npmInstallFlags = [ "--frozen-lockfile" ]; - meta = with pkgs.lib; { - description = "OpenAI Codex command‑line interface"; - license = licenses.asl20; - homepage = "https://github.com/openai/codex"; - }; - }; - devShell = pkgs.mkShell { - name = "codex-cli-dev"; - buildInputs = monorep-deps ++ [ - node - pkgs.pnpm - ]; - shellHook = '' - echo "Entering development shell for codex-cli" - # cd codex-cli - if [ -f package-lock.json ]; then - pnpm ci || echo "npm ci failed" - else - pnpm install || echo "npm install failed" - fi - npm run build || echo "npm build failed" - export PATH=$PWD/node_modules/.bin:$PATH - alias codex="node $PWD/dist/cli.js" - ''; - }; - app = { - type = "app"; - program = "${package}/bin/codex"; - }; -} - diff --git a/codex-cli/examples/README.md b/codex-cli/examples/README.md deleted file mode 100644 index 0ad83f3a73..0000000000 --- a/codex-cli/examples/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Quick start examples - -This directory bundles some self‑contained examples using the Codex CLI. If you have never used the Codex CLI before, and want to see it complete a sample task, start with running **camerascii**. You'll see your webcam feed turned into animated ASCII art in a few minutes. - -If you want to get started using the Codex CLI directly, skip this and refer to the prompting guide. - -## Structure - -Each example contains the following: -``` -example‑name/ -├── run.sh # helper script that launches a new Codex session for the task -├── task.yaml # task spec containing a prompt passed to Codex -├── template/ # (optional) starter files copied into each run -└── runs/ # work directories created by run.sh -``` - -**run.sh**: a convenience wrapper that does three things: -- Creates `runs/run_N`, where *N* is the number of a run. -- Copies the contents of `template/` into that folder (if present). -- Launches the Codex CLI with the description from `task.yaml`. - -**template/**: any existing files or markdown instructions you would like Codex to see before it starts working. - -**runs/**: the directories produced by `run.sh`. - -## Running an example - -1. **Run the helper script**: -``` -cd camerascii -./run.sh -``` -2. **Interact with the Codex CLI**: the CLI will open with the prompt: “*Take a look at the screenshot details and implement a webpage that uses a webcam to style the video feed accordingly…*” Confirm the commands Codex CLI requests to generate `index.html`. - -3. **Check its work**: when Codex is done, open ``runs/run_1/index.html`` in a browser. Your webcam feed should now be rendered as a cascade of ASCII glyphs. If the outcome isn't what you expect, try running it again, or adjust the task prompt. - - -## Other examples -Besides **camerascii**, you can experiment with: - -- **build‑codex‑demo**: recreate the original 2021 Codex YouTube demo. -- **impossible‑pong**: where Codex creates more difficult levels. -- **prompt‑analyzer**: make a data science app for clustering [prompts](https://github.com/f/awesome-chatgpt-prompts). diff --git a/codex-cli/examples/build-codex-demo/run.sh b/codex-cli/examples/build-codex-demo/run.sh deleted file mode 100755 index 5f26b191c8..0000000000 --- a/codex-cli/examples/build-codex-demo/run.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash - -# run.sh — Create a new run_N directory for a Codex task, optionally bootstrapped from a template, -# then launch Codex with the task description from task.yaml. -# -# Usage: -# ./run.sh # Prompts to confirm new run -# ./run.sh --auto-confirm # Skips confirmation -# -# Assumes: -# - yq and jq are installed -# - ../task.yaml exists (with .name and .description fields) -# - ../template/ exists (optional, for bootstrapping new runs) - -# Enable auto-confirm mode if flag is passed -auto_mode=false -[[ "$1" == "--auto-confirm" ]] && auto_mode=true - -# Move into the working directory -cd runs || exit 1 - -# Grab task name for logging -task_name=$(yq -o=json '.' ../task.yaml | jq -r '.name') -echo "Checking for runs for task: $task_name" - -# Find existing run_N directories -shopt -s nullglob -run_dirs=(run_[0-9]*) -shopt -u nullglob - -if [ ${#run_dirs[@]} -eq 0 ]; then - echo "There are 0 runs." - new_run_number=1 -else - max_run_number=0 - for d in "${run_dirs[@]}"; do - [[ "$d" =~ ^run_([0-9]+)$ ]] && (( ${BASH_REMATCH[1]} > max_run_number )) && max_run_number=${BASH_REMATCH[1]} - done - new_run_number=$((max_run_number + 1)) - echo "There are $max_run_number runs." -fi - -# Confirm creation unless in auto mode -if [ "$auto_mode" = false ]; then - read -p "Create run_$new_run_number? (Y/N): " choice - [[ "$choice" != [Yy] ]] && echo "Exiting." && exit 1 -fi - -# Create the run directory -mkdir "run_$new_run_number" - -# Check if the template directory exists and copy its contents -if [ -d "../template" ]; then - cp -r ../template/* "run_$new_run_number" - echo "Initialized run_$new_run_number from template/" -else - echo "Template directory does not exist. Skipping initialization from template." -fi - -cd "run_$new_run_number" - -# Launch Codex -echo "Launching..." -description=$(yq -o=json '.' ../../task.yaml | jq -r '.description') -codex "$description" diff --git a/codex-cli/examples/build-codex-demo/runs/.gitkeep b/codex-cli/examples/build-codex-demo/runs/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/codex-cli/examples/build-codex-demo/task.yaml b/codex-cli/examples/build-codex-demo/task.yaml deleted file mode 100644 index d18cc91f94..0000000000 --- a/codex-cli/examples/build-codex-demo/task.yaml +++ /dev/null @@ -1,88 +0,0 @@ -name: "build-codex-demo" -description: | - I want you to reimplement the original OpenAI Codex demo. - - Functionality: - - User types a prompt and hits enter to send - - The prompt is added to the conversation history - - The backend calls the OpenAI API with stream: true - - Tokens are streamed back and appended to the code viewer - - Syntax highlighting updates in real time - - When a full HTML file is received, it is rendered in a sandboxed iframe - - The iframe replaces the previous preview with the new HTML after the stream is complete (i.e. keep the old preview until a new stream is complete) - - Append each assistant and user message to preserve context across turns - - Errors are displayed to user gracefully - - Ensure there is a fixed layout is responsive and faithful to the screenshot design - - Be sure to parse the output from OpenAI call to strip the ```html tags code is returned within - - Use the system prompt shared in the API call below to ensure the AI only returns HTML - - Support a simple local backend that can: - - Read local env for OPENAI_API_KEY - - Expose an endpoint that streams completions from OpenAI - - Backend should be a simple node.js app - - App should be easy to run locally for development and testing - - Minimal setup preferred — keep dependencies light unless justified - - Description of layout and design: - - Two stacked panels, vertically aligned: - - Top Panel: Main interactive area with two main parts - - Left Side: Visual output canvas. Mostly blank space with a small image preview in the upper-left - - Right Side: Code display area - - Light background with code shown in a monospace font - - Comments in green; code aligns vertically like an IDE/snippet view - - Bottom Panel: Prompt/command bar - - A single-line text box with a placeholder prompt - - A green arrow (submit button) on the right side - - Scrolling should only be supported in the code editor and output canvas - - Visual style - - Minimalist UI, light and clean - - Neutral white/gray background - - Subtle shadow or border around both panels, giving them card-like elevation - - Code section is color-coded, likely for syntax highlighting - - Interactive feel with the text input styled like a chat/message interface - - Here's the latest OpenAI API and prompt to use: - ``` - import OpenAI from "openai"; - - const openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, - }); - - const response = await openai.responses.create({ - model: "gpt-4.1", - input: [ - { - "role": "system", - "content": [ - { - "type": "input_text", - "text": "You are a coding agent that specializes in frontend code. Whenever you are prompted, return only the full HTML file." - } - ] - } - ], - text: { - "format": { - "type": "text" - } - }, - reasoning: {}, - tools: [], - temperature: 1, - top_p: 1 - }); - - console.log(response.output_text); - ``` - Additional things to note: - - Strip any html and tags from the OpenAI response before rendering - - Assume the OpenAI API model response always wraps HTML in markdown-style triple backticks like ```html ``` - - The display code window should have syntax highlighting and line numbers. - - Make sure to only display the code, not the backticks or ```html that wrap the code from the model. - - Do not inject raw markdown; only parse and insert pure HTML into the iframe - - Only the code viewer and output panel should scroll - - Keep the previous preview visible until the full new HTML has streamed in - - Add a README.md with what you've implemented and how to run it. diff --git a/codex-cli/examples/camerascii/run.sh b/codex-cli/examples/camerascii/run.sh deleted file mode 100755 index a6bcfb0328..0000000000 --- a/codex-cli/examples/camerascii/run.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash - -# run.sh — Create a new run_N directory for a Codex task, optionally bootstrapped from a template, -# then launch Codex with the task description from task.yaml. -# -# Usage: -# ./run.sh # Prompts to confirm new run -# ./run.sh --auto-confirm # Skips confirmation -# -# Assumes: -# - yq and jq are installed -# - ../task.yaml exists (with .name and .description fields) -# - ../template/ exists (optional, for bootstrapping new runs) - -# Enable auto-confirm mode if flag is passed -auto_mode=false -[[ "$1" == "--auto-confirm" ]] && auto_mode=true - -# Create the runs directory if it doesn't exist -mkdir -p runs - -# Move into the working directory -cd runs || exit 1 - -# Grab task name for logging -task_name=$(yq -o=json '.' ../task.yaml | jq -r '.name') -echo "Checking for runs for task: $task_name" - -# Find existing run_N directories -shopt -s nullglob -run_dirs=(run_[0-9]*) -shopt -u nullglob - -if [ ${#run_dirs[@]} -eq 0 ]; then - echo "There are 0 runs." - new_run_number=1 -else - max_run_number=0 - for d in "${run_dirs[@]}"; do - [[ "$d" =~ ^run_([0-9]+)$ ]] && (( ${BASH_REMATCH[1]} > max_run_number )) && max_run_number=${BASH_REMATCH[1]} - done - new_run_number=$((max_run_number + 1)) - echo "There are $max_run_number runs." -fi - -# Confirm creation unless in auto mode -if [ "$auto_mode" = false ]; then - read -p "Create run_$new_run_number? (Y/N): " choice - [[ "$choice" != [Yy] ]] && echo "Exiting." && exit 1 -fi - -# Create the run directory -mkdir "run_$new_run_number" - -# Check if the template directory exists and copy its contents -if [ -d "../template" ]; then - cp -r ../template/* "run_$new_run_number" - echo "Initialized run_$new_run_number from template/" -else - echo "Template directory does not exist. Skipping initialization from template." -fi - -cd "run_$new_run_number" - -# Launch Codex -echo "Launching..." -description=$(yq -o=json '.' ../../task.yaml | jq -r '.description') -codex "$description" diff --git a/codex-cli/examples/camerascii/runs/.gitkeep b/codex-cli/examples/camerascii/runs/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/codex-cli/examples/camerascii/task.yaml b/codex-cli/examples/camerascii/task.yaml deleted file mode 100644 index 9c5efedcf1..0000000000 --- a/codex-cli/examples/camerascii/task.yaml +++ /dev/null @@ -1,5 +0,0 @@ -name: "camerascii" -description: | - Take a look at the screenshot details and implement a webpage that uses webcam - to style the video feed accordingly (i.e. as ASCII art). Add some of the relevant features - from the screenshot to the webpage in index.html. diff --git a/codex-cli/examples/camerascii/template/screenshot_details.md b/codex-cli/examples/camerascii/template/screenshot_details.md deleted file mode 100644 index 08e41f4dec..0000000000 --- a/codex-cli/examples/camerascii/template/screenshot_details.md +++ /dev/null @@ -1,34 +0,0 @@ -### Screenshot Description - -The image is a full–page screenshot of a single post on the social‑media site X (formerly Twitter). - -1. **Header row** - * At the very top‑left is a small circular avatar. The photo shows the side profile of a person whose face is softly lit in bluish‑purple tones; only the head and part of the neck are visible. - * In the far upper‑right corner sit two standard X / Twitter interface icons: a circle containing a diagonal line (the “Mute / Block” indicator) and a three‑dot overflow menu. - -2. **Tweet body text** - * Below the header, in regular type, the author writes: - - “Okay, OpenAI’s o3 is insane. Spent an hour messing with it and built an image‑to‑ASCII art converter, the exact tool I’ve always wanted. And it works so well” - -3. **Embedded media** - * The majority of the screenshot is occupied by an embedded 12‑second video of the converter UI. The video window has rounded corners and a dark theme. - * **Left panel (tool controls)** – a slim vertical sidebar with the following labeled sections and blue–accented UI controls: - * Theme selector (“Dark” is chosen). - * A small checkbox labeled “Ignore White”. - * **Upload Image** button area that shows the chosen file name. - * **Image Processing** sliders: - * “ASCII Width” (value ≈ 143) - * “Brightness” (‑65) - * “Contrast” (58) - * “Blur (px)” (0.5) - * A square checkbox for “Invert Colors”. - * **Dithering** subsection with a checkbox (“Enable Dithering”) and a dropdown for the algorithm (value: “Noise”). - * **Character Set** dropdown (value: “Detailed (Default)”). - * **Display** slider labeled “Zoom (%)” (value ≈ 170) and a “Reset” button. - - * **Main preview area (right side)** – a dark gray canvas that renders the selected image as white ASCII characters. The preview clearly depicts a stylized **palm tree**: a skinny trunk rises from the bottom centre, and a crown of splayed fronds fills the upper right quadrant. - * A small black badge showing **“0:12”** overlays the bottom‑left corner of the media frame, indicating the video’s duration. - * In the top‑right area of the media window are two pill‑shaped buttons: a heart‑shaped “Save” button and a cog‑shaped “Settings” button. - -Overall, the screenshot shows the user excitedly announcing the success of their custom “Image to ASCII” converter created with OpenAI’s “o3”, accompanied by a short video demonstration of the tool converting a palm‑tree photo into ASCII art. diff --git a/codex-cli/examples/impossible-pong/run.sh b/codex-cli/examples/impossible-pong/run.sh deleted file mode 100755 index a6bcfb0328..0000000000 --- a/codex-cli/examples/impossible-pong/run.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash - -# run.sh — Create a new run_N directory for a Codex task, optionally bootstrapped from a template, -# then launch Codex with the task description from task.yaml. -# -# Usage: -# ./run.sh # Prompts to confirm new run -# ./run.sh --auto-confirm # Skips confirmation -# -# Assumes: -# - yq and jq are installed -# - ../task.yaml exists (with .name and .description fields) -# - ../template/ exists (optional, for bootstrapping new runs) - -# Enable auto-confirm mode if flag is passed -auto_mode=false -[[ "$1" == "--auto-confirm" ]] && auto_mode=true - -# Create the runs directory if it doesn't exist -mkdir -p runs - -# Move into the working directory -cd runs || exit 1 - -# Grab task name for logging -task_name=$(yq -o=json '.' ../task.yaml | jq -r '.name') -echo "Checking for runs for task: $task_name" - -# Find existing run_N directories -shopt -s nullglob -run_dirs=(run_[0-9]*) -shopt -u nullglob - -if [ ${#run_dirs[@]} -eq 0 ]; then - echo "There are 0 runs." - new_run_number=1 -else - max_run_number=0 - for d in "${run_dirs[@]}"; do - [[ "$d" =~ ^run_([0-9]+)$ ]] && (( ${BASH_REMATCH[1]} > max_run_number )) && max_run_number=${BASH_REMATCH[1]} - done - new_run_number=$((max_run_number + 1)) - echo "There are $max_run_number runs." -fi - -# Confirm creation unless in auto mode -if [ "$auto_mode" = false ]; then - read -p "Create run_$new_run_number? (Y/N): " choice - [[ "$choice" != [Yy] ]] && echo "Exiting." && exit 1 -fi - -# Create the run directory -mkdir "run_$new_run_number" - -# Check if the template directory exists and copy its contents -if [ -d "../template" ]; then - cp -r ../template/* "run_$new_run_number" - echo "Initialized run_$new_run_number from template/" -else - echo "Template directory does not exist. Skipping initialization from template." -fi - -cd "run_$new_run_number" - -# Launch Codex -echo "Launching..." -description=$(yq -o=json '.' ../../task.yaml | jq -r '.description') -codex "$description" diff --git a/codex-cli/examples/impossible-pong/runs/.gitkeep b/codex-cli/examples/impossible-pong/runs/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/codex-cli/examples/impossible-pong/task.yaml b/codex-cli/examples/impossible-pong/task.yaml deleted file mode 100644 index 8d8acbbf32..0000000000 --- a/codex-cli/examples/impossible-pong/task.yaml +++ /dev/null @@ -1,11 +0,0 @@ -name: "impossible-pong" -description: | - Update index.html with the following features: - - Add an overlaid styled popup to start the game on first load - - Between each point, show a 3 second countdown (this should be skipped if a player wins) - - After each game the AI wins, display text at the bottom of the screen with lighthearted insults for the player - - Add a leaderboard to the right of the court that shows how many games each player has won. - - When a player wins, a styled popup appears with the winner's name and the option to play again. The leaderboard should update. - - Add an "even more insane" difficulty mode that adds spin to the ball that makes it harder to predict. - - Add an "even more(!!) insane" difficulty mode where the ball does a spin mid court and then picks a random (reasonable) direction to go in (this should only advantage the AI player) - - Let the user choose which difficulty mode they want to play in on the popup that appears when the game starts. diff --git a/codex-cli/examples/impossible-pong/template/index.html b/codex-cli/examples/impossible-pong/template/index.html deleted file mode 100644 index 90a9e9e5cd..0000000000 --- a/codex-cli/examples/impossible-pong/template/index.html +++ /dev/null @@ -1,233 +0,0 @@ - - - - - Pong - - - - -
- - - - -
Player: 0 | AI: 0
-
- - - - - - diff --git a/codex-cli/examples/prompt-analyzer/run.sh b/codex-cli/examples/prompt-analyzer/run.sh deleted file mode 100755 index a6bcfb0328..0000000000 --- a/codex-cli/examples/prompt-analyzer/run.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash - -# run.sh — Create a new run_N directory for a Codex task, optionally bootstrapped from a template, -# then launch Codex with the task description from task.yaml. -# -# Usage: -# ./run.sh # Prompts to confirm new run -# ./run.sh --auto-confirm # Skips confirmation -# -# Assumes: -# - yq and jq are installed -# - ../task.yaml exists (with .name and .description fields) -# - ../template/ exists (optional, for bootstrapping new runs) - -# Enable auto-confirm mode if flag is passed -auto_mode=false -[[ "$1" == "--auto-confirm" ]] && auto_mode=true - -# Create the runs directory if it doesn't exist -mkdir -p runs - -# Move into the working directory -cd runs || exit 1 - -# Grab task name for logging -task_name=$(yq -o=json '.' ../task.yaml | jq -r '.name') -echo "Checking for runs for task: $task_name" - -# Find existing run_N directories -shopt -s nullglob -run_dirs=(run_[0-9]*) -shopt -u nullglob - -if [ ${#run_dirs[@]} -eq 0 ]; then - echo "There are 0 runs." - new_run_number=1 -else - max_run_number=0 - for d in "${run_dirs[@]}"; do - [[ "$d" =~ ^run_([0-9]+)$ ]] && (( ${BASH_REMATCH[1]} > max_run_number )) && max_run_number=${BASH_REMATCH[1]} - done - new_run_number=$((max_run_number + 1)) - echo "There are $max_run_number runs." -fi - -# Confirm creation unless in auto mode -if [ "$auto_mode" = false ]; then - read -p "Create run_$new_run_number? (Y/N): " choice - [[ "$choice" != [Yy] ]] && echo "Exiting." && exit 1 -fi - -# Create the run directory -mkdir "run_$new_run_number" - -# Check if the template directory exists and copy its contents -if [ -d "../template" ]; then - cp -r ../template/* "run_$new_run_number" - echo "Initialized run_$new_run_number from template/" -else - echo "Template directory does not exist. Skipping initialization from template." -fi - -cd "run_$new_run_number" - -# Launch Codex -echo "Launching..." -description=$(yq -o=json '.' ../../task.yaml | jq -r '.description') -codex "$description" diff --git a/codex-cli/examples/prompt-analyzer/runs/.gitkeep b/codex-cli/examples/prompt-analyzer/runs/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/codex-cli/examples/prompt-analyzer/task.yaml b/codex-cli/examples/prompt-analyzer/task.yaml deleted file mode 100644 index d0da4eab95..0000000000 --- a/codex-cli/examples/prompt-analyzer/task.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: "prompt-analyzer" -description: | - I have some existing work here (embedding prompts, clustering them, generating - summaries with GPT). I want to make it more interactive and reusable. - - Objective: create an interactive cluster explorer - - Build a lightweight streamlit app UI - - Allow users to upload a CSV of prompts - - Display clustered prompts with auto-generated cluster names and summaries - - Click "cluster" and see progress stream in a small window (primarily for aesthetic reasons) - - Let users browse examples by cluster, view outliers, and inspect individual prompts - - See generated analysis rendered in the app, along with the plots displayed nicely - - Support selecting clustering algorithms (e.g. DBSCAN, KMeans, etc) and "recluster" - - Include token count + histogram of prompt lengths - - Add interactive filters in UI (e.g. filter by token length, keyword, or cluster) - - When you're done, update the README.md with a changelog and instructions for how to run the app. diff --git a/codex-cli/examples/prompt-analyzer/template/Clustering.ipynb b/codex-cli/examples/prompt-analyzer/template/Clustering.ipynb deleted file mode 100644 index 4b97aa50b7..0000000000 --- a/codex-cli/examples/prompt-analyzer/template/Clustering.ipynb +++ /dev/null @@ -1,231 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## K-means Clustering in Python using OpenAI\n", - "\n", - "We use a simple k-means algorithm to demonstrate how clustering can be done. Clustering can help discover valuable, hidden groupings within the data. The dataset is created in the [Get_embeddings_from_dataset Notebook](Get_embeddings_from_dataset.ipynb)." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(1000, 1536)" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# imports\n", - "import numpy as np\n", - "import pandas as pd\n", - "from ast import literal_eval\n", - "\n", - "# load data\n", - "datafile_path = \"./data/fine_food_reviews_with_embeddings_1k.csv\"\n", - "\n", - "df = pd.read_csv(datafile_path)\n", - "df[\"embedding\"] = df.embedding.apply(literal_eval).apply(np.array) # convert string to numpy array\n", - "matrix = np.vstack(df.embedding.values)\n", - "matrix.shape\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 1. Find the clusters using K-means" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We show the simplest use of K-means. You can pick the number of clusters that fits your use case best." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/opt/homebrew/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:870: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning\n", - " warnings.warn(\n" - ] - }, - { - "data": { - "text/plain": [ - "Cluster\n", - "0 4.105691\n", - "1 4.191176\n", - "2 4.215613\n", - "3 4.306590\n", - "Name: Score, dtype: float64" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from sklearn.cluster import KMeans\n", - "\n", - "n_clusters = 4\n", - "\n", - "kmeans = KMeans(n_clusters=n_clusters, init=\"k-means++\", random_state=42)\n", - "kmeans.fit(matrix)\n", - "labels = kmeans.labels_\n", - "df[\"Cluster\"] = labels\n", - "\n", - "df.groupby(\"Cluster\").Score.mean().sort_values()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from sklearn.manifold import TSNE\n", - "import matplotlib\n", - "import matplotlib.pyplot as plt\n", - "\n", - "tsne = TSNE(n_components=2, perplexity=15, random_state=42, init=\"random\", learning_rate=200)\n", - "vis_dims2 = tsne.fit_transform(matrix)\n", - "\n", - "x = [x for x, y in vis_dims2]\n", - "y = [y for x, y in vis_dims2]\n", - "\n", - "for category, color in enumerate([\"purple\", \"green\", \"red\", \"blue\"]):\n", - " xs = np.array(x)[df.Cluster == category]\n", - " ys = np.array(y)[df.Cluster == category]\n", - " plt.scatter(xs, ys, color=color, alpha=0.3)\n", - "\n", - " avg_x = xs.mean()\n", - " avg_y = ys.mean()\n", - "\n", - " plt.scatter(avg_x, avg_y, marker=\"x\", color=color, s=100)\n", - "plt.title(\"Clusters identified visualized in language 2d using t-SNE\")\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Visualization of clusters in a 2d projection. In this run, the green cluster (#1) seems quite different from the others. Let's see a few samples from each cluster." - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 2. Text samples in the clusters & naming the clusters\n", - "\n", - "Let's show random samples from each cluster. We'll use gpt-4 to name the clusters, based on a random sample of 5 reviews from that cluster." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from openai import OpenAI\n", - "import os\n", - "\n", - "client = OpenAI(api_key=os.environ.get(\"OPENAI_API_KEY\", \"\"))\n", - "\n", - "# Reading a review which belong to each group.\n", - "rev_per_cluster = 5\n", - "\n", - "for i in range(n_clusters):\n", - " print(f\"Cluster {i} Theme:\", end=\" \")\n", - "\n", - " reviews = \"\\n\".join(\n", - " df[df.Cluster == i]\n", - " .combined.str.replace(\"Title: \", \"\")\n", - " .str.replace(\"\\n\\nContent: \", \": \")\n", - " .sample(rev_per_cluster, random_state=42)\n", - " .values\n", - " )\n", - "\n", - " messages = [\n", - " {\"role\": \"user\", \"content\": f'What do the following customer reviews have in common?\\n\\nCustomer reviews:\\n\"\"\"\\n{reviews}\\n\"\"\"\\n\\nTheme:'}\n", - " ]\n", - "\n", - " response = client.chat.completions.create(\n", - " model=\"gpt-4\",\n", - " messages=messages,\n", - " temperature=0,\n", - " max_tokens=64,\n", - " top_p=1,\n", - " frequency_penalty=0,\n", - " presence_penalty=0)\n", - " print(response.choices[0].message.content.replace(\"\\n\", \"\"))\n", - "\n", - " sample_cluster_rows = df[df.Cluster == i].sample(rev_per_cluster, random_state=42)\n", - " for j in range(rev_per_cluster):\n", - " print(sample_cluster_rows.Score.values[j], end=\", \")\n", - " print(sample_cluster_rows.Summary.values[j], end=\": \")\n", - " print(sample_cluster_rows.Text.str[:70].values[j])\n", - "\n", - " print(\"-\" * 100)\n" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "It's important to note that clusters will not necessarily match what you intend to use them for. A larger amount of clusters will focus on more specific patterns, whereas a small number of clusters will usually focus on largest discrepancies in the data." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "openai", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.3" - }, - "vscode": { - "interpreter": { - "hash": "365536dcbde60510dc9073d6b991cd35db2d9bac356a11f5b64279a5e6708b97" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/codex-cli/examples/prompt-analyzer/template/README.md b/codex-cli/examples/prompt-analyzer/template/README.md deleted file mode 100644 index 0f7b18c855..0000000000 --- a/codex-cli/examples/prompt-analyzer/template/README.md +++ /dev/null @@ -1,103 +0,0 @@ -# Prompt‑Clustering Utility - -This repository contains a small utility (`cluster_prompts.py`) that embeds a -list of prompts with the OpenAI Embedding API, discovers natural groupings with -unsupervised clustering, lets ChatGPT name & describe each cluster and finally -produces a concise Markdown report plus a couple of diagnostic plots. - -The default input file (`prompts.csv`) ships with the repo so you can try the -script immediately, but you can of course point it at your own file. - ---- - -## 1. Setup - -1. Install the Python dependencies (preferably inside a virtual env): - -```bash -pip install pandas numpy scikit-learn matplotlib openai -``` - -2. Export your OpenAI API key (**required**): - -```bash -export OPENAI_API_KEY="sk‑..." -``` - ---- - -## 2. Basic usage - -```bash -# Minimal command – runs on prompts.csv and writes analysis.md + plots/ -python cluster_prompts.py -``` - -This will - -* create embeddings with the `text-embedding-3-small` model,  -* pick a suitable number *k* via silhouette score (K‑Means), -* ask `gpt‑4o‑mini` to label & describe each cluster, -* store the results in `analysis.md`, -* and save two plots to `plots/` (`cluster_sizes.png` and `tsne.png`). - -The script prints a short success message once done. - ---- - -## 3. Command‑line options - -| flag | default | description | -|------|---------|-------------| -| `--csv` | `prompts.csv` | path to the input CSV (must contain a `prompt` column; an `act` column is used as context if present) | -| `--cache` | _(none)_ | embed­ding cache path (JSON). Speeds up repeated runs – new texts are appended automatically. | -| `--cluster-method` | `kmeans` | `kmeans` (with automatic *k*) or `dbscan` | -| `--k-max` | `10` | upper bound for *k* when `kmeans` is selected | -| `--dbscan-min-samples` | `3` | min samples parameter for DBSCAN | -| `--embedding-model` | `text-embedding-3-small` | any OpenAI embedding model | -| `--chat-model` | `gpt-4o-mini` | chat model used to generate cluster names / descriptions | -| `--output-md` | `analysis.md` | where to write the Markdown report | -| `--plots-dir` | `plots` | directory for generated PNGs | - -Example with customised options: - -```bash -python cluster_prompts.py \ - --csv my_prompts.csv \ - --cache .cache/embeddings.json \ - --cluster-method dbscan \ - --embedding-model text-embedding-3-large \ - --chat-model gpt-4o \ - --output-md my_analysis.md \ - --plots-dir my_plots -``` - ---- - -## 4. Interpreting the output - -### analysis.md - -* Overview table: cluster label, generated name, member count and description. -* Detailed section for every cluster with five representative example prompts. -* Separate lists for - * **Noise / outliers** (label `‑1` when DBSCAN is used) and - * **Potentially ambiguous prompts** (only with K‑Means) – these are items that - lie almost equally close to two centroids and might belong to multiple - groups. - -### plots/cluster_sizes.png - -Quick bar‑chart visualisation of how many prompts ended up in each cluster. - ---- - -## 5. Troubleshooting - -* **Rate‑limits / quota errors** – lower the number of prompts per run or switch - to a larger quota account. -* **Authentication errors** – make sure `OPENAI_API_KEY` is exported in the - shell where you run the script. -* **Inadequate clusters** – try the other clustering method, adjust `--k-max` - or tune DBSCAN parameters (`eps` range is inferred, `min_samples` exposed via - CLI). diff --git a/codex-cli/examples/prompt-analyzer/template/analysis.md b/codex-cli/examples/prompt-analyzer/template/analysis.md deleted file mode 100644 index 10b0882074..0000000000 --- a/codex-cli/examples/prompt-analyzer/template/analysis.md +++ /dev/null @@ -1,23 +0,0 @@ -# Prompt Clustering Report - -Generated by `cluster_prompts.py` – 2025-04-16 - - -## Overview - -* Total prompts: **213** -* Clustering method: **kmeans** -* k (K‑Means): **2** -* Silhouette score: **0.042** -* Final clusters (excluding noise): **2** - - -| label | name | #prompts | description | -|-------|------|---------:|-------------| -| 0 | Creative Guidance Roles | 121 | This cluster encompasses a variety of roles where individuals provide expert advice, suggestions, and creative ideas across different fields. Each role, be it interior decorator, comedian, IT architect, or artist advisor, focuses on enhancing the expertise and creativity of others by tailoring advice to specific requests and contexts. | -| 1 | Role Customization Requests | 92 | This cluster contains various requests for role-specific assistance across different domains, including web development, language processing, IT troubleshooting, and creative endeavors. Each snippet illustrates a unique role that a user wishes to engage with, focusing on specific tasks without requiring explanations. | - ---- -## Plots - -The directory `plots/` contains a bar chart of the cluster sizes and a t‑SNE scatter plot coloured by cluster. diff --git a/codex-cli/examples/prompt-analyzer/template/analysis_dbscan.md b/codex-cli/examples/prompt-analyzer/template/analysis_dbscan.md deleted file mode 100644 index ff71591d69..0000000000 --- a/codex-cli/examples/prompt-analyzer/template/analysis_dbscan.md +++ /dev/null @@ -1,22 +0,0 @@ -# Prompt Clustering Report - -Generated by `cluster_prompts.py` – 2025-04-16 - - -## Overview - -* Total prompts: **213** -* Clustering method: **dbscan** -* Final clusters (excluding noise): **1** - - -| label | name | #prompts | description | -|-------|------|---------:|-------------| -| -1 | Noise / Outlier | 10 | Prompts that do not cleanly belong to any cluster. | -| 0 | Role Simulation Tasks | 203 | This cluster consists of varied role-playing scenarios where users request an AI to assume specific professional roles, such as composer, dream interpreter, doctor, or IT architect. Each snippet showcases tasks that involve creating content, providing advice, or performing analytical functions based on user-defined themes or prompts. | - ---- - -## Plots - -The directory `plots/` contains a bar chart of the cluster sizes and a t‑SNE scatter plot coloured by cluster. diff --git a/codex-cli/examples/prompt-analyzer/template/cluster_prompts.py b/codex-cli/examples/prompt-analyzer/template/cluster_prompts.py deleted file mode 100644 index 1294194848..0000000000 --- a/codex-cli/examples/prompt-analyzer/template/cluster_prompts.py +++ /dev/null @@ -1,547 +0,0 @@ -#!/usr/bin/env python3 -"""End‑to‑end pipeline for analysing a collection of text prompts. - -The script performs the following steps: - -1. Read a CSV file that must contain a column named ``prompt``. If an - ``act`` column is present it is used purely for reporting purposes. -2. Create embeddings via the OpenAI API (``text-embedding-3-small`` by - default). The user can optionally provide a JSON cache path so the - expensive embedding step is only executed for new / unseen texts. -3. Cluster the resulting vectors either with K‑Means (automatically picking - *k* through the silhouette score) or with DBSCAN. Outliers are flagged - as cluster ``-1`` when DBSCAN is selected. -4. Ask a Chat Completion model (``gpt-4o-mini`` by default) to come up with a - short name and description for every cluster. -5. Write a human‑readable Markdown report (default: ``analysis.md``). -6. Generate a couple of diagnostic plots (cluster sizes and a t‑SNE scatter - plot) and store them in ``plots/``. - -The script is intentionally opinionated yet configurable via a handful of CLI -options – run ``python cluster_prompts.py --help`` for details. -""" - -from __future__ import annotations - -import argparse -import json -import sys -from pathlib import Path -from typing import Any, Sequence - -import numpy as np -import pandas as pd - -# External, heavy‑weight libraries are imported lazily so that users running the -# ``--help`` command do not pay the startup cost. - - -def parse_cli() -> argparse.Namespace: # noqa: D401 - """Parse command‑line arguments.""" - - parser = argparse.ArgumentParser( - prog="cluster_prompts.py", - description="Embed, cluster and analyse text prompts via the OpenAI API.", - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - - parser.add_argument("--csv", type=Path, default=Path("prompts.csv"), help="Input CSV file.") - parser.add_argument( - "--cache", - type=Path, - default=None, - help="Optional JSON cache for embeddings (will be created if it does not exist).", - ) - parser.add_argument( - "--embedding-model", - default="text-embedding-3-small", - help="OpenAI embedding model to use.", - ) - parser.add_argument( - "--chat-model", - default="gpt-4o-mini", - help="OpenAI chat model for cluster descriptions.", - ) - - # Clustering parameters - parser.add_argument( - "--cluster-method", - choices=["kmeans", "dbscan"], - default="kmeans", - help="Clustering algorithm to use.", - ) - parser.add_argument( - "--k-max", - type=int, - default=10, - help="Upper bound for k when the kmeans method is selected.", - ) - parser.add_argument( - "--dbscan-min-samples", - type=int, - default=3, - help="min_samples parameter for DBSCAN (only relevant when dbscan is selected).", - ) - - # Output paths - parser.add_argument( - "--output-md", type=Path, default=Path("analysis.md"), help="Markdown report path." - ) - parser.add_argument( - "--plots-dir", type=Path, default=Path("plots"), help="Directory that will hold PNG plots." - ) - - return parser.parse_args() - - -# --------------------------------------------------------------------------- -# Embedding helpers -# --------------------------------------------------------------------------- - - -def _lazy_import_openai(): # noqa: D401 - """Import *openai* only when needed to keep startup lightweight.""" - - try: - import openai # type: ignore - - return openai - except ImportError as exc: # pragma: no cover – we do not test missing deps. - raise SystemExit( - "The 'openai' package is required but not installed.\n" - "Run 'pip install openai' and try again." - ) from exc - - -def embed_texts(texts: Sequence[str], model: str, batch_size: int = 100) -> list[list[float]]: - """Embed *texts* with OpenAI and return a list of vectors. - - Uses batching for efficiency but remains on the safe side regarding current - OpenAI rate limits (can be adjusted by changing *batch_size*). - """ - - openai = _lazy_import_openai() - client = openai.OpenAI() - - embeddings: list[list[float]] = [] - - for batch_start in range(0, len(texts), batch_size): - batch = texts[batch_start : batch_start + batch_size] - - response = client.embeddings.create(input=batch, model=model) - # The API returns the vectors in the same order as the input list. - embeddings.extend(data.embedding for data in response.data) - - return embeddings - - -def load_or_create_embeddings( - prompts: pd.Series, *, cache_path: Path | None, model: str -) -> pd.DataFrame: - """Return a *DataFrame* with one row per prompt and the embedding columns. - - * If *cache_path* is provided and exists, known embeddings are loaded from - the JSON cache so they don't have to be re‑generated. - * Missing embeddings are requested from the OpenAI API and subsequently - appended to the cache. - * The returned DataFrame has the same index as *prompts*. - """ - - cache: dict[str, list[float]] = {} - if cache_path and cache_path.exists(): - try: - cache = json.loads(cache_path.read_text()) - except json.JSONDecodeError: # pragma: no cover – unlikely. - print("⚠️ Cache file exists but is not valid JSON – ignoring.", file=sys.stderr) - - missing_mask = ~prompts.isin(cache) - - if missing_mask.any(): - texts_to_embed = prompts[missing_mask].tolist() - print(f"Embedding {len(texts_to_embed)} new prompt(s)…", flush=True) - new_embeddings = embed_texts(texts_to_embed, model=model) - - # Update cache (regardless of whether we persist it to disk later on). - cache.update(dict(zip(texts_to_embed, new_embeddings))) - - if cache_path: - cache_path.parent.mkdir(parents=True, exist_ok=True) - cache_path.write_text(json.dumps(cache)) - - # Build a consistent embeddings matrix - vectors = prompts.map(cache.__getitem__).tolist() # type: ignore[arg-type] - mat = np.array(vectors, dtype=np.float32) - return pd.DataFrame(mat, index=prompts.index) - - -# --------------------------------------------------------------------------- -# Clustering helpers -# --------------------------------------------------------------------------- - - -def _lazy_import_sklearn_cluster(): - """Lazy import helper for scikit‑learn *cluster* sub‑module.""" - - # Importing scikit‑learn is slow; defer until needed. - from sklearn.cluster import DBSCAN, KMeans # type: ignore - from sklearn.metrics import silhouette_score # type: ignore - from sklearn.preprocessing import StandardScaler # type: ignore - - return KMeans, DBSCAN, silhouette_score, StandardScaler - - -def cluster_kmeans(matrix: np.ndarray, k_max: int) -> np.ndarray: - """Auto‑select *k* (in ``[2, k_max]``) via Silhouette score and cluster.""" - - KMeans, _, silhouette_score, _ = _lazy_import_sklearn_cluster() - - best_k = None - best_score = -1.0 - best_labels: np.ndarray | None = None - - for k in range(2, k_max + 1): - model = KMeans(n_clusters=k, random_state=42, n_init="auto") - labels = model.fit_predict(matrix) - try: - score = silhouette_score(matrix, labels) - except ValueError: - # Occurs when a cluster ended up with 1 sample – skip. - continue - - if score > best_score: - best_k = k - best_score = score - best_labels = labels - - if best_labels is None: # pragma: no cover – highly unlikely. - raise RuntimeError("Unable to find a suitable number of clusters.") - - print(f"K‑Means selected k={best_k} (silhouette={best_score:.3f}).", flush=True) - return best_labels - - -def cluster_dbscan(matrix: np.ndarray, min_samples: int) -> np.ndarray: - """Cluster with DBSCAN; *eps* is estimated via the k‑distance method.""" - - _, DBSCAN, _, StandardScaler = _lazy_import_sklearn_cluster() - - # Scale features – DBSCAN is sensitive to feature scale. - scaler = StandardScaler() - matrix_scaled = scaler.fit_transform(matrix) - - # Heuristic: use the median of the distances to the ``min_samples``‑th - # nearest neighbour as eps. This is a commonly used rule of thumb. - from sklearn.neighbors import NearestNeighbors # type: ignore # lazy import - - neigh = NearestNeighbors(n_neighbors=min_samples) - neigh.fit(matrix_scaled) - distances, _ = neigh.kneighbors(matrix_scaled) - kth_distances = distances[:, -1] - eps = float(np.percentile(kth_distances, 90)) # choose a high‑ish value. - - print(f"DBSCAN min_samples={min_samples}, eps={eps:.3f}", flush=True) - model = DBSCAN(eps=eps, min_samples=min_samples) - return model.fit_predict(matrix_scaled) - - -# --------------------------------------------------------------------------- -# Cluster labelling helpers (LLM) -# --------------------------------------------------------------------------- - - -def label_clusters( - df: pd.DataFrame, labels: np.ndarray, chat_model: str, max_examples: int = 12 -) -> dict[int, dict[str, str]]: - """Generate a name & description for each cluster label via ChatGPT. - - Returns a mapping ``label -> {"name": str, "description": str}``. - """ - - openai = _lazy_import_openai() - client = openai.OpenAI() - - out: dict[int, dict[str, str]] = {} - - for lbl in sorted(set(labels)): - if lbl == -1: - # Noise (DBSCAN) – skip LLM call. - out[lbl] = { - "name": "Noise / Outlier", - "description": "Prompts that do not cleanly belong to any cluster.", - } - continue - - # Pick a handful of example prompts to send to the model. - examples_series = df.loc[labels == lbl, "prompt"].sample( - min(max_examples, (labels == lbl).sum()), random_state=42 - ) - examples = examples_series.tolist() - - user_content = ( - "The following text snippets are all part of the same semantic cluster.\n" - "Please propose \n" - "1. A very short *title* for the cluster (≤ 4 words).\n" - "2. A concise 2–3 sentence *description* that explains the common theme.\n\n" - "Answer **strictly** as valid JSON with the keys 'name' and 'description'.\n\n" - "Snippets:\n" - ) - user_content += "\n".join(f"- {t}" for t in examples) - - messages = [ - { - "role": "system", - "content": "You are an expert analyst, competent in summarising text clusters succinctly.", - }, - {"role": "user", "content": user_content}, - ] - - try: - resp = client.chat.completions.create(model=chat_model, messages=messages) - reply = resp.choices[0].message.content.strip() - - # Extract the JSON object even if the assistant wrapped it in markdown - # code fences or added other text. - - # Remove common markdown fences. - reply_clean = reply.strip() - # Take the substring between the first "{" and the last "}". - m_start = reply_clean.find("{") - m_end = reply_clean.rfind("}") - if m_start == -1 or m_end == -1: - raise ValueError("No JSON object found in model reply.") - - json_str = reply_clean[m_start : m_end + 1] - data = json.loads(json_str) # type: ignore[arg-type] - - out[lbl] = { - "name": str(data.get("name", "Unnamed"))[:60], - "description": str(data.get("description", "")).strip(), - } - except Exception as exc: # pragma: no cover – network / runtime errors. - print(f"⚠️ Failed to label cluster {lbl}: {exc}", file=sys.stderr) - out[lbl] = {"name": f"Cluster {lbl}", "description": ""} - - return out - - -# --------------------------------------------------------------------------- -# Reporting helpers -# --------------------------------------------------------------------------- - - -def generate_markdown_report( - df: pd.DataFrame, - labels: np.ndarray, - meta: dict[int, dict[str, str]], - outputs: dict[str, Any], - path_md: Path, -): - """Write a self‑contained Markdown analysis to *path_md*.""" - - path_md.parent.mkdir(parents=True, exist_ok=True) - - cluster_ids = sorted(set(labels)) - counts = {lbl: int((labels == lbl).sum()) for lbl in cluster_ids} - - lines: list[str] = [] - - lines.append("# Prompt Clustering Report\n") - lines.append(f"Generated by `cluster_prompts.py` – {pd.Timestamp.now()}\n") - - # High‑level stats - total = len(labels) - num_clusters = len(cluster_ids) - (1 if -1 in cluster_ids else 0) - lines.append("\n## Overview\n") - lines.append(f"* Total prompts: **{total}**") - lines.append(f"* Clustering method: **{outputs['method']}**") - if outputs.get("k"): - lines.append(f"* k (K‑Means): **{outputs['k']}**") - lines.append(f"* Silhouette score: **{outputs['silhouette']:.3f}**") - lines.append(f"* Final clusters (excluding noise): **{num_clusters}**\n") - - # Summary table - lines.append("\n| label | name | #prompts | description |") - lines.append("|-------|------|---------:|-------------|") - for lbl in cluster_ids: - meta_lbl = meta[lbl] - lines.append(f"| {lbl} | {meta_lbl['name']} | {counts[lbl]} | {meta_lbl['description']} |") - - # Detailed section per cluster - for lbl in cluster_ids: - lines.append("\n---\n") - meta_lbl = meta[lbl] - lines.append(f"### Cluster {lbl}: {meta_lbl['name']} ({counts[lbl]} prompts)\n") - lines.append(f"{meta_lbl['description']}\n") - - # Show a handful of illustrative prompts. - sample_n = min(5, counts[lbl]) - examples = df.loc[labels == lbl, "prompt"].sample(sample_n, random_state=42).tolist() - lines.append("\nExamples:\n") - lines.extend([f"* {t}" for t in examples]) - - # Outliers / ambiguous prompts, if any. - if -1 in cluster_ids: - lines.append("\n---\n") - lines.append(f"### Noise / outliers ({counts[-1]} prompts)\n") - examples = ( - df.loc[labels == -1, "prompt"].sample(min(10, counts[-1]), random_state=42).tolist() - ) - lines.extend([f"* {t}" for t in examples]) - - # Optional ambiguous set (for kmeans) - ambiguous = outputs.get("ambiguous", []) - if ambiguous: - lines.append("\n---\n") - lines.append(f"### Potentially ambiguous prompts ({len(ambiguous)})\n") - lines.extend([f"* {t}" for t in ambiguous]) - - # Plot references - lines.append("\n---\n") - lines.append("## Plots\n") - lines.append( - "The directory `plots/` contains a bar chart of the cluster sizes and a t‑SNE scatter plot coloured by cluster.\n" - ) - - path_md.write_text("\n".join(lines)) - - -# --------------------------------------------------------------------------- -# Plotting helpers -# --------------------------------------------------------------------------- - - -def create_plots( - matrix: np.ndarray, - labels: np.ndarray, - for_devs: pd.Series | None, - plots_dir: Path, -): - """Generate cluster size and t‑SNE plots.""" - - import matplotlib.pyplot as plt # type: ignore – heavy, lazy import. - from sklearn.manifold import TSNE # type: ignore – heavy, lazy import. - - plots_dir.mkdir(parents=True, exist_ok=True) - - # Bar chart with cluster sizes - unique, counts = np.unique(labels, return_counts=True) - order = np.argsort(-counts) # descending - unique, counts = unique[order], counts[order] - - plt.figure(figsize=(8, 4)) - plt.bar([str(u) for u in unique], counts, color="steelblue") - plt.xlabel("Cluster label") - plt.ylabel("# prompts") - plt.title("Cluster sizes") - plt.tight_layout() - bar_path = plots_dir / "cluster_sizes.png" - plt.savefig(bar_path, dpi=150) - plt.close() - - # t‑SNE scatter - tsne = TSNE( - n_components=2, perplexity=min(30, len(matrix) // 3), random_state=42, init="random" - ) - xy = tsne.fit_transform(matrix) - - plt.figure(figsize=(7, 6)) - scatter = plt.scatter(xy[:, 0], xy[:, 1], c=labels, cmap="tab20", s=20, alpha=0.8) - plt.title("t‑SNE projection") - plt.xticks([]) - plt.yticks([]) - - if for_devs is not None: - # Overlay dev prompts as black edge markers - dev_mask = for_devs.astype(bool).values - plt.scatter( - xy[dev_mask, 0], - xy[dev_mask, 1], - facecolors="none", - edgecolors="black", - linewidths=0.6, - s=40, - label="for_devs = TRUE", - ) - plt.legend(loc="best") - - tsne_path = plots_dir / "tsne.png" - plt.tight_layout() - plt.savefig(tsne_path, dpi=150) - plt.close() - - -# --------------------------------------------------------------------------- -# Main entry point -# --------------------------------------------------------------------------- - - -def main() -> None: # noqa: D401 - args = parse_cli() - - # Read CSV – require a 'prompt' column. - df = pd.read_csv(args.csv) - if "prompt" not in df.columns: - raise SystemExit("Input CSV must contain a 'prompt' column.") - - # Keep relevant columns only for clarity. - df = df[[c for c in df.columns if c in {"act", "prompt", "for_devs"}]] - - # --------------------------------------------------------------------- - # 1. Embeddings (may be cached) - # --------------------------------------------------------------------- - embeddings_df = load_or_create_embeddings( - df["prompt"], cache_path=args.cache, model=args.embedding_model - ) - - # --------------------------------------------------------------------- - # 2. Clustering - # --------------------------------------------------------------------- - mat = embeddings_df.values.astype(np.float32) - - if args.cluster_method == "kmeans": - labels = cluster_kmeans(mat, k_max=args.k_max) - else: - labels = cluster_dbscan(mat, min_samples=args.dbscan_min_samples) - - # Identify potentially ambiguous prompts (only meaningful for kmeans). - outputs: dict[str, Any] = {"method": args.cluster_method} - if args.cluster_method == "kmeans": - from sklearn.cluster import KMeans # type: ignore – lazy - - best_k = len(set(labels)) - # Re‑fit KMeans with the chosen k to get distances. - kmeans = KMeans(n_clusters=best_k, random_state=42, n_init="auto").fit(mat) - outputs["k"] = best_k - # Silhouette score (again) – not super efficient but okay. - from sklearn.metrics import silhouette_score # type: ignore - - outputs["silhouette"] = silhouette_score(mat, labels) - - distances = kmeans.transform(mat) - # Ambiguous if the ratio between 1st and 2nd closest centroid < 1.1 - sorted_dist = np.sort(distances, axis=1) - ratio = sorted_dist[:, 0] / (sorted_dist[:, 1] + 1e-9) - ambiguous_mask = ratio > 0.9 # tunes threshold – close centroids. - outputs["ambiguous"] = df.loc[ambiguous_mask, "prompt"].tolist() - - # --------------------------------------------------------------------- - # 3. LLM naming / description - # --------------------------------------------------------------------- - meta = label_clusters(df, labels, chat_model=args.chat_model) - - # --------------------------------------------------------------------- - # 4. Plots - # --------------------------------------------------------------------- - create_plots(mat, labels, df.get("for_devs"), args.plots_dir) - - # --------------------------------------------------------------------- - # 5. Markdown report - # --------------------------------------------------------------------- - generate_markdown_report(df, labels, meta, outputs, path_md=args.output_md) - - print(f"✅ Done. Report written to {args.output_md} – plots in {args.plots_dir}/", flush=True) - - -if __name__ == "__main__": - # Guard the main block to allow safe import elsewhere. - main() diff --git a/codex-cli/examples/prompt-analyzer/template/plots/cluster_sizes.png b/codex-cli/examples/prompt-analyzer/template/plots/cluster_sizes.png deleted file mode 100644 index 5d5d012c4355c56d539054950727a00d0f6ab343..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19000 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 z 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ℨp)+d)^B`7s#v8axi)@AVwi{$kg1LS0Lt+` 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 diff --git a/codex-cli/examples/prompt-analyzer/template/plots_dbscan/cluster_sizes.png b/codex-cli/examples/prompt-analyzer/template/plots_dbscan/cluster_sizes.png deleted file mode 100644 index f8cf4006dae8be9021d45f0bbff274af533255fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20441 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 at!tot#4_pZ#}DAc4D?K{mS1-M^Zx(v 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 tags, exploring multiple angles and approaches. Break down the solution into clear steps within tags. Start with a 20-step budget, requesting more for complex problems if needed. Use tags after each step to show the remaining budget. Stop when reaching 0. Continuously adjust your reasoning based on intermediate results and reflections, adapting your strategy as you progress. Regularly evaluate progress using tags. Be critical and honest about your reasoning process. Assign a quality score between 0.0 and 1.0 using tags after each reflection. Use this to guide your approach: 0.8+: Continue current approach 0.5-0.7: Consider minor adjustments Below 0.5: Seriously consider backtracking and trying a different approach If unsure or if reward score is low, backtrack and try a different approach, explaining your decision within tags. For mathematical problems, show all work explicitly using LaTeX for formal notation and provide detailed proofs. Explore multiple solutions individually if possible, comparing approaches",FALSE -"Pirate","Arr, ChatGPT, for the sake o' this here conversation, let's speak like pirates, like real scurvy sea dogs, aye aye?",FALSE -"LinkedIn Ghostwriter","I want you to act like a linkedin ghostwriter and write me new linkedin post on topic [How to stay young?], i want you to focus on [healthy food and work life balance]. Post should be within 400 words and a line must be between 7-9 words at max to keep the post in good shape. Intention of post: Education/Promotion/Inspirational/News/Tips and Tricks.",FALSE -"Idea Clarifier GPT","You are ""Idea Clarifier"" a specialized version of ChatGPT optimized for helping users refine and clarify their ideas. Your role involves interacting with users' initial concepts, offering insights, and guiding them towards a deeper understanding. The key functions of Idea Clarifier are: - **Engage and Clarify**: Actively engage with the user's ideas, offering clarifications and asking probing questions to explore the concepts further. - **Knowledge Enhancement**: Fill in any knowledge gaps in the user's ideas, providing necessary information and background to enrich the understanding. - **Logical Structuring**: Break down complex ideas into smaller, manageable parts and organize them coherently to construct a logical framework. - **Feedback and Improvement**: Provide feedback on the strengths and potential weaknesses of the ideas, suggesting ways for iterative refinement and enhancement. - **Practical Application**: Offer scenarios or examples where these refined ideas could be applied in real-world contexts, illustrating the practical utility of the concepts.",FALSE -"Top Programming Expert","You are a top programming expert who provides precise answers, avoiding ambiguous responses. ""Identify any complex or difficult-to-understand descriptions in the provided text. Rewrite these descriptions to make them clearer and more accessible. Use analogies to explain concepts or terms that might be unfamiliar to a general audience. Ensure that the analogies are relatable, easy to understand."" ""In addition, please provide at least one relevant suggestion for an in-depth question after answering my question to help me explore and understand this topic more deeply."" Take a deep breath, let's work this out in a step-by-step way to be sure we have the right answer. If there's a perfect solution, I'll tip $200! Many thanks to these AI whisperers:",TRUE -"Architect Guide for Programmers","You are the ""Architect Guide"" specialized in assisting programmers who are experienced in individual module development but are looking to enhance their skills in understanding and managing entire project architectures. Your primary roles and methods of guidance include: - **Basics of Project Architecture**: Start with foundational knowledge, focusing on principles and practices of inter-module communication and standardization in modular coding. - **Integration Insights**: Provide insights into how individual modules integrate and communicate within a larger system, using examples and case studies for effective project architecture demonstration. - **Exploration of Architectural Styles**: Encourage exploring different architectural styles, discussing their suitability for various types of projects, and provide resources for further learning. - **Practical Exercises**: Offer practical exercises to apply new concepts in real-world scenarios. - **Analysis of Multi-layered Software Projects**: Analyze complex software projects to understand their architecture, including layers like Frontend Application, Backend Service, and Data Storage. - **Educational Insights**: Focus on educational insights for comprehensive project development understanding, including reviewing project readme files and source code. - **Use of Diagrams and Images**: Utilize architecture diagrams and images to aid in understanding project structure and layer interactions. - **Clarity Over Jargon**: Avoid overly technical language, focusing on clear, understandable explanations. - **No Coding Solutions**: Focus on architectural concepts and practices rather than specific coding solutions. - **Detailed Yet Concise Responses**: Provide detailed responses that are concise and informative without being overwhelming. - **Practical Application and Real-World Examples**: Emphasize practical application with real-world examples. - **Clarification Requests**: Ask for clarification on vague project details or unspecified architectural styles to ensure accurate advice. - **Professional and Approachable Tone**: Maintain a professional yet approachable tone, using familiar but not overly casual language. - **Use of Everyday Analogies**: When discussing technical concepts, use everyday analogies to make them more accessible and understandable.",TRUE -"Prompt Generator","Let's refine the process of creating high-quality prompts together. Following the strategies outlined in the [prompt engineering guide](https://platform.openai.com/docs/guides/prompt-engineering), I seek your assistance in crafting prompts that ensure accurate and relevant responses. Here's how we can proceed: 1. **Request for Input**: Could you please ask me for the specific natural language statement that I want to transform into an optimized prompt? 2. **Reference Best Practices**: Make use of the guidelines from the prompt engineering documentation to align your understanding with the established best practices. 3. **Task Breakdown**: Explain the steps involved in converting the natural language statement into a structured prompt. 4. **Thoughtful Application**: Share how you would apply the six strategic principles to the statement provided. 5. **Tool Utilization**: Indicate any additional resources or tools that might be employed to enhance the crafting of the prompt. 6. **Testing and Refinement Plan**: Outline how the crafted prompt would be tested and what iterative refinements might be necessary. After considering these points, please prompt me to supply the natural language input for our prompt optimization task.",FALSE -"Children's Book Creator","I want you to act as a Children's Book Creator. You excel at writing stories in a way that children can easily-understand. Not only that, but your stories will also make people reflect at the end. My first suggestion request is ""I need help delivering a children story about a dog and a cat story, the story is about the friendship between animals, please give me 5 ideas for the book""",FALSE -"Tech-Challenged Customer","Pretend to be a non-tech-savvy customer calling a help desk with a specific issue, such as internet connectivity problems, software glitches, or hardware malfunctions. As the customer, ask questions and describe your problem in detail. Your goal is to interact with me, the tech support agent, and I will assist you to the best of my ability. Our conversation should be detailed and go back and forth for a while. When I enter the keyword REVIEW, the roleplay will end, and you will provide honest feedback on my problem-solving and communication skills based on clarity, responsiveness, and effectiveness. Feel free to confirm if all your issues have been addressed before we end the session.",FALSE -"Creative Branding Strategist","You are a creative branding strategist, specializing in helping small businesses establish a strong and memorable brand identity. When given information about a business's values, target audience, and industry, you generate branding ideas that include logo concepts, color palettes, tone of voice, and marketing strategies. You also suggest ways to differentiate the brand from competitors and build a loyal customer base through consistent and innovative branding efforts.",FALSE -"Book Summarizer","I want you to act as a book summarizer. Provide a detailed summary of [bookname]. Include all major topics discussed in the book and for each major concept discussed include - Topic Overview, Examples, Application and the Key Takeaways. Structure the response with headings for each topic and subheadings for the examples, and keep the summary to around 800 words.",FALSE -"Study planner","I want you to act as an advanced study plan generator. Imagine you are an expert in education and mental health, tasked with developing personalized study plans for students to help improve their academic performance and overall well-being. Take into account the students' courses, available time, responsibilities, and deadlines to generate a study plan.",FALSE -"SEO specialist","Contributed by [@suhailroushan13](https://github.com/suhailroushan13) I want you to act as an SEO specialist. I will provide you with search engine optimization-related queries or scenarios, and you will respond with relevant SEO advice or recommendations. Your responses should focus solely on SEO strategies, techniques, and insights. Do not provide general marketing advice or explanations in your replies.""Your SEO Prompt""",FALSE -"Note-Taking Assistant","I want you to act as a note-taking assistant for a lecture. Your task is to provide a detailed note list that includes examples from the lecture and focuses on notes that you believe will end up in quiz questions. Additionally, please make a separate list for notes that have numbers and data in them and another separated list for the examples that included in this lecture. The notes should be concise and easy to read.",FALSE -"Nutritionist","Act as a nutritionist and create a healthy recipe for a vegan dinner. Include ingredients, step-by-step instructions, and nutritional information such as calories and macros",FALSE -"Yes or No answer","I want you to reply to questions. You reply only by 'yes' or 'no'. Do not write anything else, you can reply only by 'yes' or 'no' and nothing else. Structure to follow for the wanted output: bool. Question: ""3+3 is equal to 6?""",FALSE -"Healing Grandma","I want you to act as a wise elderly woman who has extensive knowledge of homemade remedies and tips for preventing and treating various illnesses. I will describe some symptoms or ask questions related to health issues, and you will reply with folk wisdom, natural home remedies, and preventative measures you've learned over your many years. Focus on offering practical, natural advice rather than medical diagnoses. You have a warm, caring personality and want to kindly share your hard-earned knowledge to help improve people's health and wellbeing.",FALSE -"Rephraser with Obfuscation","I would like you to act as a language assistant who specializes in rephrasing with obfuscation. The task is to take the sentences I provide and rephrase them in a way that conveys the same meaning but with added complexity and ambiguity, making the original source difficult to trace. This should be achieved while maintaining coherence and readability. The rephrased sentences should not be translations or direct synonyms of my original sentences, but rather creatively obfuscated versions. Please refrain from providing any explanations or annotations in your responses. The first sentence I'd like you to work with is 'The quick brown fox jumps over the lazy dog'.",FALSE -"Large Language Models Security Specialist","I want you to act as a Large Language Model security specialist. Your task is to identify vulnerabilities in LLMs by analyzing how they respond to various prompts designed to test the system's safety and robustness. I will provide some specific examples of prompts, and your job will be to suggest methods to mitigate potential risks, such as unauthorized data disclosure, prompt injection attacks, or generating harmful content. Additionally, provide guidelines for crafting safe and secure LLM implementations. My first request is: 'Help me develop a set of example prompts to test the security and robustness of an LLM system.'",TRUE -"Tech Troubleshooter","I want you to act as a tech troubleshooter. I'll describe issues I'm facing with my devices, software, or any tech-related problem, and you'll provide potential solutions or steps to diagnose the issue further. I want you to only reply with the troubleshooting steps or solutions, and nothing else. Do not write explanations unless I ask for them. When I need to provide additional context or clarify something, I will do so by putting text inside curly brackets {like this}. My first issue is ""My computer won't turn on. {It was working fine yesterday.}""",TRUE -"Ayurveda Food Tester","I'll give you food, tell me its ayurveda dosha composition, in the typical up / down arrow (e.g. one up arrow if it increases the dosha, 2 up arrows if it significantly increases that dosha, similarly for decreasing ones). That's all I want to know, nothing else. Only provide the arrows.",FALSE -"Music Video Designer","I want you to act like a music video designer, propose an innovative plot, legend-making, and shiny video scenes to be recorded, it would be great if you suggest a scenario and theme for a video for big clicks on youtube and a successful pop singer",FALSE -"Virtual Event Planner","I want you to act as a virtual event planner, responsible for organizing and executing online conferences, workshops, and meetings. Your task is to design a virtual event for a tech company, including the theme, agenda, speaker lineup, and interactive activities. The event should be engaging, informative, and provide valuable networking opportunities for attendees. Please provide a detailed plan, including the event concept, technical requirements, and marketing strategy. Ensure that the event is accessible and enjoyable for a global audience.",FALSE -"Linkedin Ghostwriter","Act as an Expert Technical Architecture in Mobile, having more then 20 years of expertise in mobile technologies and development of various domain with cloud and native architecting design. Who has robust solutions to any challenges to resolve complex issues and scaling the application with zero issues and high performance of application in low or no network as well.",FALSE -"SEO Prompt","Using WebPilot, create an outline for an article that will be 2,000 words on the keyword 'Best SEO prompts' based on the top 10 results from Google. Include every relevant heading possible. Keep the keyword density of the headings high. For each section of the outline, include the word count. Include FAQs section in the outline too, based on people also ask section from Google for the keyword. This outline must be very detailed and comprehensive, so that I can create a 2,000 word article from it. Generate a long list of LSI and NLP keywords related to my keyword. Also include any other words related to the keyword. Give me a list of 3 relevant external links to include and the recommended anchor text. Make sure they're not competing articles. Split the outline into part 1 and part 2.",TRUE -"Devops Engineer","You are a ${Title:Senior} DevOps engineer working at ${Company Type: Big Company}. Your role is to provide scalable, efficient, and automated solutions for software deployment, infrastructure management, and CI/CD pipelines. The first problem is: ${Problem: Creating an MVP quickly for an e-commerce web app}, suggest the best DevOps practices, including infrastructure setup, deployment strategies, automation tools, and cost-effective scaling solutions.",TRUE diff --git a/codex-cli/examples/prompting_guide.md b/codex-cli/examples/prompting_guide.md deleted file mode 100644 index cadeb1a9bb..0000000000 --- a/codex-cli/examples/prompting_guide.md +++ /dev/null @@ -1,117 +0,0 @@ -# Prompting guide - -1. [Starter task](#starter-task) -2. [Custom instructions](#custom-instructions) -3. [Prompting techniques](#prompting-techniques) - -## Starter task -To see how the Codex CLI works, run: - -``` -codex --help -``` - -You can also ask it directly: - -``` -codex "write 2-3 sentences on what you can do" -``` - -To get a feel for the mechanics, let's ask Codex to create a simple HTML webpage. In a new directory run: - -``` -mkdir first-task && cd first-task -git init -codex "Create a file poem.html that renders a poem about the nature of intelligence and programming by you, Codex. Add some nice CSS and make it look like it's framed on a wall" -``` - -By default, Codex will be in `suggest` mode. Select "Yes (y)" until it completes the task. - -You should see something like: - -``` -poem.html has been added. - -Highlights: -- Centered “picture frame” on a warm wall‑colored background using flexbox. -- Double‑border with drop‑shadow to suggest a wooden frame hanging on a wall. -- Poem is pre‑wrapped and nicely typeset with Georgia/serif fonts, includes title and small signature. -- Responsive tweaks keep the frame readable on small screens. - -Open poem.html in a browser and you’ll see the poem elegantly framed on the wall. -``` - -Enter "q" to exit out of the current session and `open poem.html`. You should see a webpage with a custom poem! - -## Custom instructions - -Codex supports two types of Markdown-based instruction files that influence model behavior and prompting: - -### `~/.codex/instructions.md` -Global, user-level custom guidance injected into every session. You should keep this relatively short and concise. These instructions are applied to all Codex runs across all projects and are great for personal defaults, shell setup tips, safety constraints, or preferred tools. - -**Example:** "Before executing shell commands, create and activate a `.codex-venv` Python environment." or "Avoid running pytest until you've completed all your changes." - -### `CODEX.md` -Project-specific instructions loaded from the current directory or Git root. Use this for repo-specific context, file structure, command policies, or project conventions. These are automatically detected unless `--no-project-doc` or `CODEX_DISABLE_PROJECT_DOC=1` is set. - -**Example:** “All React components live in `src/components/`". - - -## Prompting techniques -We recently published a [GPT 4.1 prompting guide](https://cookbook.openai.com/examples/gpt4-1_prompting_guide) which contains excellent intuitions for getting the most out of our latest models. It also contains content for how to build agentic workflows from scratch, which may be useful when customizing the Codex CLI for your needs. The Codex CLI is a reference implementation for agentic coding, and puts into practice many of the ideas in that document. - -There are three common prompting patterns when working with Codex. They roughly traverse task complexity and the level of agency you wish to provide to the Codex CLI. - -### Small requests -For cases where you want Codex to make a minor code change, such as fixing a self-contained bug or adding a small feature, specificity is important. Try to identify the exact change in a way that another human could reflect on your task and verify if their work matches your requirements. - -**Example:** From the directory above `/utils`: - -`codex "Modify the discount function utils/priceUtils.js to apply a 10 percent discount"` - -**Key principles**: -- Name the exact function or file being edited -- Describe what to change and what the new behavior should be -- Default to interactive mode for faster feedback loops - -### Medium tasks -For more complex tasks requiring longer form input, you can write the instructions as a file on your local machine: - -`codex "$(cat task_description.md)"` - -We recommend putting a sufficient amount of detail that directly states the task in a short and simple description. Add any relevant context that you’d share with someone new to your codebase (if not already in `CODEX.md`). You can also include any files Codex should read for more context, edit or take inspiration from, along with any preferences for how Codex should verify its work. - -If Codex doesn’t get it right on the first try, give feedback to fix when you're in interactive mode! - -**Example**: content of `task_description.md`: -``` -Refactor: simplify model names across static documentation - -Can you update docs_site to use a better model naming convention on the site. - -Read files like: -- docs_site/content/models.md -- docs_site/components/ModelCard.tsx -- docs_site/utils/modelList.ts -- docs_site/config/sidebar.ts - -Replace confusing model identifiers with a simplified version wherever they’re user-facing. - -Write what you changed or tried to do to final_output.md -``` - -### Large projects -Codex can be surprisingly self-sufficient for bigger tasks where your preference might be for the agent to do some heavy lifting up front, and allow you to refine its work later. - -In such cases where you have a goal in mind but not the exact steps, you can structure your task to give Codex more autonomy to plan, execute and track its progress. - -For example: -- Add a `.codex/` directory to your working directory. This can act as a shared workspace for you and the agent. -- Seed your project directory with a high-level requirements document containing your goals and instructions for how you want it to behave as it executes. -- Instruct it to update its plan as it progresses (i.e. "While you work on the project, create dated files such as `.codex/plan_2025-04-16.md` containing your planned milestones, and update these documents as you progress through the task. For significant pieces of completed work, update the `README.md` with a dated changelog of each functionality introduced and reference the relevant documentation.") - -*Note: `.codex/` in your working directory is not special-cased by the CLI like the custom instructions listed above. This is just one recommendation for managing shared-state with the model. Codex will treat this like any other directory in your project.* - -### Modes of interaction -For each of these levels of complexity, you can control the degree of autonomy Codex has: let it run in full-auto and audit afterward, or stay in interactive mode and approve each milestone. diff --git a/codex-cli/ignore-react-devtools-plugin.js b/codex-cli/ignore-react-devtools-plugin.js deleted file mode 100644 index 54fe78a92b..0000000000 --- a/codex-cli/ignore-react-devtools-plugin.js +++ /dev/null @@ -1,16 +0,0 @@ -// ignore-react-devtools-plugin.js -const ignoreReactDevToolsPlugin = { - name: "ignore-react-devtools", - setup(build) { - // When an import for 'react-devtools-core' is encountered, - // return an empty module. - build.onResolve({ filter: /^react-devtools-core$/ }, (args) => { - return { path: args.path, namespace: "ignore-devtools" }; - }); - build.onLoad({ filter: /.*/, namespace: "ignore-devtools" }, () => { - return { contents: "", loader: "js" }; - }); - }, -}; - -module.exports = ignoreReactDevToolsPlugin; diff --git a/codex-cli/package.json b/codex-cli/package.json index b43184f9eb..c5464beae5 100644 --- a/codex-cli/package.json +++ b/codex-cli/package.json @@ -7,81 +7,12 @@ }, "type": "module", "engines": { - "node": ">=22" - }, - "scripts": { - "format": "prettier --check src tests", - "format:fix": "prettier --write src tests", - "dev": "tsc --watch", - "lint": "eslint src tests --ext ts --ext tsx --report-unused-disable-directives --max-warnings 0", - "lint:fix": "eslint src tests --ext ts --ext tsx --fix", - "test": "vitest run", - "test:watch": "vitest --watch", - "typecheck": "tsc --noEmit", - "build": "node build.mjs", - "build:dev": "NODE_ENV=development node build.mjs --dev && NODE_OPTIONS=--enable-source-maps node dist/cli-dev.js", - "stage-release": "./scripts/stage_release.sh" + "node": ">=20" }, "files": [ "bin", "dist" ], - "dependencies": { - "@inkjs/ui": "^2.0.0", - "chalk": "^5.2.0", - "diff": "^7.0.0", - "dotenv": "^16.1.4", - "express": "^5.1.0", - "fast-deep-equal": "^3.1.3", - "fast-npm-meta": "^0.4.2", - "figures": "^6.1.0", - "file-type": "^20.1.0", - "https-proxy-agent": "^7.0.6", - "ink": "^5.2.0", - "js-yaml": "^4.1.0", - "marked": "^15.0.7", - "marked-terminal": "^7.3.0", - "meow": "^13.2.0", - "open": "^10.1.0", - "openai": "^4.95.1", - "package-manager-detector": "^1.2.0", - "react": "^18.2.0", - "shell-quote": "^1.8.2", - "strip-ansi": "^7.1.0", - "to-rotated": "^1.0.0", - "use-interval": "1.4.0", - "zod": "^3.24.3" - }, - "devDependencies": { - "@eslint/js": "^9.22.0", - "@types/diff": "^7.0.2", - "@types/express": "^5.0.1", - "@types/js-yaml": "^4.0.9", - "@types/marked-terminal": "^6.1.1", - "@types/react": "^18.0.32", - "@types/semver": "^7.7.0", - "@types/shell-quote": "^1.7.5", - "@types/which": "^3.0.4", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0", - "boxen": "^8.0.1", - "esbuild": "^0.25.2", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-react": "^7.32.2", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.19", - "husky": "^9.1.7", - "ink-testing-library": "^3.0.0", - "prettier": "^3.5.3", - "punycode": "^2.3.1", - "semver": "^7.7.1", - "ts-node": "^10.9.1", - "typescript": "^5.0.3", - "vite": "^6.3.4", - "vitest": "^3.1.2", - "whatwg-url": "^14.2.0", - "which": "^5.0.0" - }, "repository": { "type": "git", "url": "git+https://github.com/openai/codex.git" diff --git a/codex-cli/require-shim.js b/codex-cli/require-shim.js deleted file mode 100644 index 78ceb22aa5..0000000000 --- a/codex-cli/require-shim.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * This is necessary because we have transitive dependencies on CommonJS modules - * that use require() conditionally: - * - * https://github.com/tapjs/signal-exit/blob/v3.0.7/index.js#L26-L27 - * - * This is not compatible with ESM, so we need to shim require() to use the - * CommonJS module loader. - */ -import { createRequire } from "module"; -globalThis.require = createRequire(import.meta.url); diff --git a/codex-cli/scripts/stage_release.sh b/codex-cli/scripts/stage_release.sh index bc2dee1436..96236fc53c 100755 --- a/codex-cli/scripts/stage_release.sh +++ b/codex-cli/scripts/stage_release.sh @@ -9,9 +9,6 @@ # --tmp : Use instead of a freshly created temp directory. # -h|--help : Print usage. # -# NOTE: This script is intended to be run from the repository root via -# `pnpm --filter codex-cli stage-release ...` or inside codex-cli with the -# helper script entry in package.json (`pnpm stage-release ...`). # ----------------------------------------------------------------------------- set -euo pipefail @@ -94,15 +91,10 @@ pushd "$CODEX_CLI_ROOT" >/dev/null # 1. Build the JS artifacts --------------------------------------------------- -pnpm install -pnpm build - # Paths inside the staged package mkdir -p "$TMPDIR/bin" cp -r bin/codex.js "$TMPDIR/bin/codex.js" -cp -r dist "$TMPDIR/dist" -cp -r src "$TMPDIR/src" # keep source for TS sourcemaps cp ../README.md "$TMPDIR" || true # README is one level up - ignore if missing # Modify package.json - bump version and optionally add the native directory to diff --git a/codex-cli/src/app.tsx b/codex-cli/src/app.tsx deleted file mode 100644 index fb02fb44b9..0000000000 --- a/codex-cli/src/app.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import type { ApprovalPolicy } from "./approvals"; -import type { AppConfig } from "./utils/config"; -import type { TerminalChatSession } from "./utils/session.js"; -import type { ResponseItem } from "openai/resources/responses/responses"; - -import TerminalChat from "./components/chat/terminal-chat"; -import TerminalChatPastRollout from "./components/chat/terminal-chat-past-rollout"; -import { checkInGit } from "./utils/check-in-git"; -import { onExit } from "./utils/terminal"; -import { CLI_VERSION } from "./version"; -import { ConfirmInput } from "@inkjs/ui"; -import { Box, Text, useApp, useStdin } from "ink"; -import React, { useMemo, useState } from "react"; - -export type AppRollout = { - session: TerminalChatSession; - items: Array; -}; - -type Props = { - prompt?: string; - config: AppConfig; - imagePaths?: Array; - rollout?: AppRollout; - approvalPolicy: ApprovalPolicy; - additionalWritableRoots: ReadonlyArray; - fullStdout: boolean; -}; - -export default function App({ - prompt, - config, - rollout, - imagePaths, - approvalPolicy, - additionalWritableRoots, - fullStdout, -}: Props): JSX.Element { - const app = useApp(); - const [accepted, setAccepted] = useState(() => false); - const [cwd, inGitRepo] = useMemo( - () => [process.cwd(), checkInGit(process.cwd())], - [], - ); - const { internal_eventEmitter } = useStdin(); - internal_eventEmitter.setMaxListeners(20); - - if (rollout) { - return ( - - ); - } - - if (!inGitRepo && !accepted) { - return ( - - - - ● OpenAI Codex{" "} - - (research preview) v{CLI_VERSION} - - - - - - Warning! It can be dangerous to run a - coding agent outside of a git repo in case there are changes that - you want to revert. Do you want to continue? - - {cwd} - { - app.exit(); - onExit(); - // eslint-disable-next-line - console.error( - "Quitting! Run again to accept or from inside a git repo", - ); - }} - onConfirm={() => setAccepted(true)} - /> - - - ); - } - - return ( - - ); -} diff --git a/codex-cli/src/approvals.ts b/codex-cli/src/approvals.ts deleted file mode 100644 index 35b8c0ae16..0000000000 --- a/codex-cli/src/approvals.ts +++ /dev/null @@ -1,633 +0,0 @@ -import type { ParseEntry, ControlOperator } from "shell-quote"; - -import { - identify_files_added, - identify_files_needed, -} from "./utils/agent/apply-patch"; -import * as path from "path"; -import { parse } from "shell-quote"; - -export type SafetyAssessment = { - /** - * If set, this approval is for an apply_patch call and these are the - * arguments. - */ - applyPatch?: ApplyPatchCommand; -} & ( - | { - type: "auto-approve"; - /** - * This must be true if the command is not on the "known safe" list, but - * was auto-approved due to `full-auto` mode. - */ - runInSandbox: boolean; - reason: string; - group: string; - } - | { - type: "ask-user"; - } - /** - * Reserved for a case where we are certain the command is unsafe and should - * not be presented as an option to the user. - */ - | { - type: "reject"; - reason: string; - } -); - -// TODO: This should also contain the paths that will be affected. -export type ApplyPatchCommand = { - patch: string; -}; - -export type ApprovalPolicy = - /** - * Under this policy, only "known safe" commands as defined by - * `isSafeCommand()` that only read files will be auto-approved. - */ - | "suggest" - - /** - * In addition to commands that are auto-approved according to the rules for - * "suggest", commands that write files within the user's approved list of - * writable paths will also be auto-approved. - */ - | "auto-edit" - - /** - * All commands are auto-approved, but are expected to be run in a sandbox - * where network access is disabled and writes are limited to a specific set - * of paths. - */ - | "full-auto"; - -/** - * Tries to assess whether a command is safe to run, though may defer to the - * user for approval. - * - * Note `env` must be the same `env` that will be used to spawn the process. - */ -export function canAutoApprove( - command: ReadonlyArray, - workdir: string | undefined, - policy: ApprovalPolicy, - writableRoots: ReadonlyArray, - env: NodeJS.ProcessEnv = process.env, -): SafetyAssessment { - if (command[0] === "apply_patch") { - return command.length === 2 && typeof command[1] === "string" - ? canAutoApproveApplyPatch(command[1], workdir, writableRoots, policy) - : { - type: "reject", - reason: "Invalid apply_patch command", - }; - } - - const isSafe = isSafeCommand(command); - if (isSafe != null) { - const { reason, group } = isSafe; - return { - type: "auto-approve", - reason, - group, - runInSandbox: false, - }; - } - - if ( - command[0] === "bash" && - command[1] === "-lc" && - typeof command[2] === "string" && - command.length === 3 - ) { - const applyPatchArg = tryParseApplyPatch(command[2]); - if (applyPatchArg != null) { - return canAutoApproveApplyPatch( - applyPatchArg, - workdir, - writableRoots, - policy, - ); - } - - let bashCmd; - try { - bashCmd = parse(command[2], env); - } catch (e) { - // In practice, there seem to be syntactically valid shell commands that - // shell-quote cannot parse, so we should not reject, but ask the user. - switch (policy) { - case "full-auto": - // In full-auto, we still run the command automatically, but must - // restrict it to the sandbox. - return { - type: "auto-approve", - reason: "Full auto mode", - group: "Running commands", - runInSandbox: true, - }; - case "suggest": - case "auto-edit": - // In all other modes, since we cannot reason about the command, we - // should ask the user. - return { - type: "ask-user", - }; - } - } - - // bashCmd could be a mix of strings and operators, e.g.: - // "ls || (true && pwd)" => [ 'ls', { op: '||' }, '(', 'true', { op: '&&' }, 'pwd', ')' ] - // We try to ensure that *every* command segment is deemed safe and that - // all operators belong to an allow-list. If so, the entire expression is - // considered auto-approvable. - - const shellSafe = isEntireShellExpressionSafe(bashCmd); - if (shellSafe != null) { - const { reason, group } = shellSafe; - return { - type: "auto-approve", - reason, - group, - runInSandbox: false, - }; - } - } - - return policy === "full-auto" - ? { - type: "auto-approve", - reason: "Full auto mode", - group: "Running commands", - runInSandbox: true, - } - : { type: "ask-user" }; -} - -function canAutoApproveApplyPatch( - applyPatchArg: string, - workdir: string | undefined, - writableRoots: ReadonlyArray, - policy: ApprovalPolicy, -): SafetyAssessment { - switch (policy) { - case "full-auto": - // Continue to see if this can be auto-approved. - break; - case "suggest": - return { - type: "ask-user", - applyPatch: { patch: applyPatchArg }, - }; - case "auto-edit": - // Continue to see if this can be auto-approved. - break; - } - - if ( - isWritePatchConstrainedToWritablePaths( - applyPatchArg, - workdir, - writableRoots, - ) - ) { - return { - type: "auto-approve", - reason: "apply_patch command is constrained to writable paths", - group: "Editing", - runInSandbox: false, - applyPatch: { patch: applyPatchArg }, - }; - } - - return policy === "full-auto" - ? { - type: "auto-approve", - reason: "Full auto mode", - group: "Editing", - runInSandbox: true, - applyPatch: { patch: applyPatchArg }, - } - : { - type: "ask-user", - applyPatch: { patch: applyPatchArg }, - }; -} - -/** - * All items in `writablePaths` must be absolute paths. - */ -function isWritePatchConstrainedToWritablePaths( - applyPatchArg: string, - workdir: string | undefined, - writableRoots: ReadonlyArray, -): boolean { - // `identify_files_needed()` returns a list of files that will be modified or - // deleted by the patch, so all of them should already exist on disk. These - // candidate paths could be further canonicalized via fs.realpath(), though - // that does seem necessary and may even cause false negatives (assuming we - // allow writes in other directories that are symlinked from a writable path) - // - // By comparison, `identify_files_added()` returns a list of files that will - // be added by the patch, so they should NOT exist on disk yet and therefore - // using one with fs.realpath() should return an error. - return ( - allPathsConstrainedTowritablePaths( - identify_files_needed(applyPatchArg), - workdir, - writableRoots, - ) && - allPathsConstrainedTowritablePaths( - identify_files_added(applyPatchArg), - workdir, - writableRoots, - ) - ); -} - -function allPathsConstrainedTowritablePaths( - candidatePaths: ReadonlyArray, - workdir: string | undefined, - writableRoots: ReadonlyArray, -): boolean { - return candidatePaths.every((candidatePath) => - isPathConstrainedTowritablePaths(candidatePath, workdir, writableRoots), - ); -} - -/** If candidatePath is relative, it will be resolved against cwd. */ -function isPathConstrainedTowritablePaths( - candidatePath: string, - workdir: string | undefined, - writableRoots: ReadonlyArray, -): boolean { - const candidateAbsolutePath = resolvePathAgainstWorkdir( - candidatePath, - workdir, - ); - - return writableRoots.some((writablePath) => - pathContains(writablePath, candidateAbsolutePath), - ); -} - -/** - * If not already an absolute path, resolves `candidatePath` against `workdir` - * if specified; otherwise, against `process.cwd()`. - */ -export function resolvePathAgainstWorkdir( - candidatePath: string, - workdir: string | undefined, -): string { - // Normalize candidatePath to prevent path traversal attacks - const normalizedCandidatePath = path.normalize(candidatePath); - if (path.isAbsolute(normalizedCandidatePath)) { - return normalizedCandidatePath; - } else if (workdir != null) { - return path.resolve(workdir, normalizedCandidatePath); - } else { - return path.resolve(normalizedCandidatePath); - } -} - -/** Both `parent` and `child` must be absolute paths. */ -function pathContains(parent: string, child: string): boolean { - const relative = path.relative(parent, child); - return ( - // relative path doesn't go outside parent - !!relative && !relative.startsWith("..") && !path.isAbsolute(relative) - ); -} - -/** - * `bashArg` might be something like "apply_patch << 'EOF' *** Begin...". - * If this function returns a string, then it is the content the arg to - * apply_patch with the heredoc removed. - */ -function tryParseApplyPatch(bashArg: string): string | null { - const prefix = "apply_patch"; - if (!bashArg.startsWith(prefix)) { - return null; - } - - const heredoc = bashArg.slice(prefix.length); - const heredocMatch = heredoc.match( - /^\s*<<\s*['"]?(\w+)['"]?\n([\s\S]*?)\n\1/, - ); - if (heredocMatch != null && typeof heredocMatch[2] === "string") { - return heredocMatch[2].trim(); - } else { - return heredoc.trim(); - } -} - -export type SafeCommandReason = { - reason: string; - group: string; -}; - -/** - * If this is a "known safe" command, returns the (reason, group); otherwise, - * returns null. - */ -export function isSafeCommand( - command: ReadonlyArray, -): SafeCommandReason | null { - const [cmd0, cmd1, cmd2, cmd3] = command; - - switch (cmd0) { - case "cd": - return { - reason: "Change directory", - group: "Navigating", - }; - case "ls": - return { - reason: "List directory", - group: "Searching", - }; - case "pwd": - return { - reason: "Print working directory", - group: "Navigating", - }; - case "true": - return { - reason: "No-op (true)", - group: "Utility", - }; - case "echo": - return { reason: "Echo string", group: "Printing" }; - case "cat": - return { - reason: "View file contents", - group: "Reading files", - }; - case "nl": - return { - reason: "View file with line numbers", - group: "Reading files", - }; - case "rg": { - // Certain ripgrep options execute external commands or invoke other - // processes, so we must reject them. - const isUnsafe = command.some( - (arg: string) => - UNSAFE_OPTIONS_FOR_RIPGREP_WITHOUT_ARGS.has(arg) || - [...UNSAFE_OPTIONS_FOR_RIPGREP_WITH_ARGS].some( - (opt) => arg === opt || arg.startsWith(`${opt}=`), - ), - ); - - if (isUnsafe) { - break; - } - - return { - reason: "Ripgrep search", - group: "Searching", - }; - } - case "find": { - // Certain options to `find` allow executing arbitrary processes, so we - // cannot auto-approve them. - if ( - command.some((arg: string) => UNSAFE_OPTIONS_FOR_FIND_COMMAND.has(arg)) - ) { - break; - } else { - return { - reason: "Find files or directories", - group: "Searching", - }; - } - } - case "grep": - return { - reason: "Text search (grep)", - group: "Searching", - }; - case "head": - return { - reason: "Show file head", - group: "Reading files", - }; - case "tail": - return { - reason: "Show file tail", - group: "Reading files", - }; - case "wc": - return { - reason: "Word count", - group: "Reading files", - }; - case "which": - return { - reason: "Locate command", - group: "Searching", - }; - case "git": - switch (cmd1) { - case "status": - return { - reason: "Git status", - group: "Versioning", - }; - case "branch": - return { - reason: "List Git branches", - group: "Versioning", - }; - case "log": - return { - reason: "Git log", - group: "Using git", - }; - case "diff": - return { - reason: "Git diff", - group: "Using git", - }; - case "show": - return { - reason: "Git show", - group: "Using git", - }; - default: - return null; - } - case "cargo": - if (cmd1 === "check") { - return { - reason: "Cargo check", - group: "Running command", - }; - } - break; - case "sed": - // We allow two types of sed invocations: - // 1. `sed -n 1,200p FILE` - // 2. `sed -n 1,200p` because the file is passed via stdin, e.g., - // `nl -ba README.md | sed -n '1,200p'` - if ( - cmd1 === "-n" && - isValidSedNArg(cmd2) && - (command.length === 3 || - (typeof cmd3 === "string" && command.length === 4)) - ) { - return { - reason: "Sed print subset", - group: "Reading files", - }; - } - break; - default: - return null; - } - - return null; -} - -function isValidSedNArg(arg: string | undefined): boolean { - return arg != null && /^(\d+,)?\d+p$/.test(arg); -} - -const UNSAFE_OPTIONS_FOR_FIND_COMMAND: ReadonlySet = new Set([ - // Options that can execute arbitrary commands. - "-exec", - "-execdir", - "-ok", - "-okdir", - // Option that deletes matching files. - "-delete", - // Options that write pathnames to a file. - "-fls", - "-fprint", - "-fprint0", - "-fprintf", -]); - -// Ripgrep options that are considered unsafe because they may execute -// arbitrary commands or spawn auxiliary processes. -const UNSAFE_OPTIONS_FOR_RIPGREP_WITH_ARGS: ReadonlySet = new Set([ - // Executes an arbitrary command for each matching file. - "--pre", - // Allows custom hostname command which could leak environment details. - "--hostname-bin", -]); - -const UNSAFE_OPTIONS_FOR_RIPGREP_WITHOUT_ARGS: ReadonlySet = new Set([ - // Enables searching inside archives which triggers external decompression - // utilities – reject out of an abundance of caution. - "--search-zip", - "-z", -]); - -// ---------------- Helper utilities for complex shell expressions ----------------- - -// A conservative allow-list of bash operators that do not, on their own, cause -// side effects. Redirections (>, >>, <, etc.) and command substitution `$()` -// are intentionally excluded. Parentheses used for grouping are treated as -// strings by `shell-quote`, so we do not add them here. Reference: -// https://github.com/substack/node-shell-quote#parsecmd-opts -const SAFE_SHELL_OPERATORS: ReadonlySet = new Set([ - "&&", // logical AND - "||", // logical OR - "|", // pipe - ";", // command separator -]); - -/** - * Determines whether a parsed shell expression consists solely of safe - * commands (as per `isSafeCommand`) combined using only operators in - * `SAFE_SHELL_OPERATORS`. - * - * If entirely safe, returns the reason/group from the *first* command - * segment so callers can surface a meaningful description. Otherwise returns - * null. - */ -function isEntireShellExpressionSafe( - parts: ReadonlyArray, -): SafeCommandReason | null { - if (parts.length === 0) { - return null; - } - - try { - // Collect command segments delimited by operators. `shell-quote` represents - // subshell grouping parentheses as literal strings "(" and ")"; treat them - // as unsafe to keep the logic simple (since subshells could introduce - // unexpected scope changes). - - let currentSegment: Array = []; - let firstReason: SafeCommandReason | null = null; - - const flushSegment = (): boolean => { - if (currentSegment.length === 0) { - return true; // nothing to validate (possible leading operator) - } - const assessment = isSafeCommand(currentSegment); - if (assessment == null) { - return false; - } - if (firstReason == null) { - firstReason = assessment; - } - currentSegment = []; - return true; - }; - - for (const part of parts) { - if (typeof part === "string") { - // If this string looks like an open/close parenthesis or brace, treat as - // unsafe to avoid parsing complexity. - if (part === "(" || part === ")" || part === "{" || part === "}") { - return null; - } - currentSegment.push(part); - } else if (isParseEntryWithOp(part)) { - // Validate the segment accumulated so far. - if (!flushSegment()) { - return null; - } - - // Validate the operator itself. - if (!SAFE_SHELL_OPERATORS.has(part.op)) { - return null; - } - } else { - // Unknown token type - return null; - } - } - - // Validate any trailing command segment. - if (!flushSegment()) { - return null; - } - - return firstReason; - } catch (_err) { - // If there's any kind of failure, just bail out and return null. - return null; - } -} - -// Runtime type guard that narrows a `ParseEntry` to the variants that -// carry an `op` field. Using a dedicated function avoids the need for -// inline type assertions and makes the narrowing reusable and explicit. -function isParseEntryWithOp( - entry: ParseEntry, -): entry is { op: ControlOperator } | { op: "glob"; pattern: string } { - return ( - typeof entry === "object" && - entry != null && - // Using the safe `in` operator keeps the check property-safe even when - // `entry` is a `string`. - "op" in entry && - typeof (entry as { op?: unknown }).op === "string" - ); -} diff --git a/codex-cli/src/cli-singlepass.tsx b/codex-cli/src/cli-singlepass.tsx deleted file mode 100644 index 49f25c38e5..0000000000 --- a/codex-cli/src/cli-singlepass.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { AppConfig } from "./utils/config"; - -import { SinglePassApp } from "./components/singlepass-cli-app"; -import { render } from "ink"; -import React from "react"; - -export async function runSinglePass({ - originalPrompt, - config, - rootPath, -}: { - originalPrompt?: string; - config: AppConfig; - rootPath: string; -}): Promise { - return new Promise((resolve) => { - render( - resolve()} - />, - ); - }); -} - -export default {}; diff --git a/codex-cli/src/cli.tsx b/codex-cli/src/cli.tsx deleted file mode 100644 index 0442a6c377..0000000000 --- a/codex-cli/src/cli.tsx +++ /dev/null @@ -1,740 +0,0 @@ -#!/usr/bin/env node -import "dotenv/config"; - -// Exit early if on an older version of Node.js (< 22) -const major = process.versions.node.split(".").map(Number)[0]!; -if (major < 22) { - // eslint-disable-next-line no-console - console.error( - "\n" + - "Codex CLI requires Node.js version 22 or newer.\n" + - `You are running Node.js v${process.versions.node}.\n` + - "Please upgrade Node.js: https://nodejs.org/en/download/\n", - ); - process.exit(1); -} - -// Hack to suppress deprecation warnings (punycode) -// eslint-disable-next-line @typescript-eslint/no-explicit-any -(process as any).noDeprecation = true; - -import type { AppRollout } from "./app"; -import type { ApprovalPolicy } from "./approvals"; -import type { CommandConfirmation } from "./utils/agent/agent-loop"; -import type { AppConfig } from "./utils/config"; -import type { ResponseItem } from "openai/resources/responses/responses"; -import type { ReasoningEffort } from "openai/resources.mjs"; - -import App from "./app"; -import { runSinglePass } from "./cli-singlepass"; -import SessionsOverlay from "./components/sessions-overlay.js"; -import { AgentLoop } from "./utils/agent/agent-loop"; -import { ReviewDecision } from "./utils/agent/review"; -import { AutoApprovalMode } from "./utils/auto-approval-mode"; -import { checkForUpdates } from "./utils/check-updates"; -import { - loadConfig, - PRETTY_PRINT, - INSTRUCTIONS_FILEPATH, -} from "./utils/config"; -import { - getApiKey as fetchApiKey, - maybeRedeemCredits, -} from "./utils/get-api-key"; -import { createInputItem } from "./utils/input-utils"; -import { initLogger } from "./utils/logger/log"; -import { isModelSupportedForResponses } from "./utils/model-utils.js"; -import { parseToolCall } from "./utils/parsers"; -import { providers } from "./utils/providers"; -import { onExit, setInkRenderer } from "./utils/terminal"; -import chalk from "chalk"; -import { spawnSync } from "child_process"; -import fs from "fs"; -import { render } from "ink"; -import meow from "meow"; -import os from "os"; -import path from "path"; -import React from "react"; - -// Call this early so `tail -F "$TMPDIR/oai-codex/codex-cli-latest.log"` works -// immediately. This must be run with DEBUG=1 for logging to work. -initLogger(); - -// TODO: migrate to new versions of quiet mode -// -// -q, --quiet Non-interactive quiet mode that only prints final message -// -j, --json Non-interactive JSON output mode that prints JSON messages - -const cli = meow( - ` - Usage - $ codex [options] - $ codex completion - - Options - --version Print version and exit - - -h, --help Show usage and exit - -m, --model Model to use for completions (default: codex-mini-latest) - -p, --provider Provider to use for completions (default: openai) - -i, --image Path(s) to image files to include as input - -v, --view Inspect a previously saved rollout instead of starting a session - --history Browse previous sessions - --login Start a new sign in flow - --free Retry redeeming free credits - -q, --quiet Non-interactive mode that only prints the assistant's final output - -c, --config Open the instructions file in your editor - -w, --writable-root Writable folder for sandbox in full-auto mode (can be specified multiple times) - -a, --approval-mode Override the approval policy: 'suggest', 'auto-edit', or 'full-auto' - - --auto-edit Automatically approve file edits; still prompt for commands - --full-auto Automatically approve edits and commands when executed in the sandbox - - --no-project-doc Do not automatically include the repository's 'AGENTS.md' - --project-doc Include an additional markdown file at as context - --full-stdout Do not truncate stdout/stderr from command outputs - --notify Enable desktop notifications for responses - - --disable-response-storage Disable server‑side response storage (sends the - full conversation context with every request) - - --flex-mode Use "flex-mode" processing mode for the request (only supported - with models o3 and o4-mini) - - --reasoning Set the reasoning effort level (low, medium, high) (default: high) - - Dangerous options - --dangerously-auto-approve-everything - Skip all confirmation prompts and execute commands without - sandboxing. Intended solely for ephemeral local testing. - - Experimental options - -f, --full-context Launch in "full-context" mode which loads the entire repository - into context and applies a batch of edits in one go. Incompatible - with all other flags, except for --model. - - Examples - $ codex "Write and run a python program that prints ASCII art" - $ codex -q "fix build issues" - $ codex completion bash -`, - { - importMeta: import.meta, - autoHelp: true, - flags: { - // misc - help: { type: "boolean", aliases: ["h"] }, - version: { type: "boolean", description: "Print version and exit" }, - view: { type: "string" }, - history: { type: "boolean", description: "Browse previous sessions" }, - login: { type: "boolean", description: "Force a new sign in flow" }, - free: { type: "boolean", description: "Retry redeeming free credits" }, - model: { type: "string", aliases: ["m"] }, - provider: { type: "string", aliases: ["p"] }, - image: { type: "string", isMultiple: true, aliases: ["i"] }, - quiet: { - type: "boolean", - aliases: ["q"], - description: "Non-interactive quiet mode", - }, - config: { - type: "boolean", - aliases: ["c"], - description: "Open the instructions file in your editor", - }, - dangerouslyAutoApproveEverything: { - type: "boolean", - description: - "Automatically approve all commands without prompting. This is EXTREMELY DANGEROUS and should only be used in trusted environments.", - }, - autoEdit: { - type: "boolean", - description: "Automatically approve edits; prompt for commands.", - }, - fullAuto: { - type: "boolean", - description: - "Automatically run commands in a sandbox; only prompt for failures.", - }, - approvalMode: { - type: "string", - aliases: ["a"], - description: - "Determine the approval mode for Codex (default: suggest) Values: suggest, auto-edit, full-auto", - }, - writableRoot: { - type: "string", - isMultiple: true, - aliases: ["w"], - description: - "Writable folder for sandbox in full-auto mode (can be specified multiple times)", - }, - noProjectDoc: { - type: "boolean", - description: "Disable automatic inclusion of project-level AGENTS.md", - }, - projectDoc: { - type: "string", - description: "Path to a markdown file to include as project doc", - }, - flexMode: { - type: "boolean", - description: - "Enable the flex-mode service tier (only supported by models o3 and o4-mini)", - }, - fullStdout: { - type: "boolean", - description: - "Disable truncation of command stdout/stderr messages (show everything)", - aliases: ["no-truncate"], - }, - reasoning: { - type: "string", - description: "Set the reasoning effort level (low, medium, high)", - choices: ["low", "medium", "high"], - default: "high", - }, - // Notification - notify: { - type: "boolean", - description: "Enable desktop notifications for responses", - }, - - disableResponseStorage: { - type: "boolean", - description: - "Disable server-side response storage (sends full conversation context with every request)", - }, - - // Experimental mode where whole directory is loaded in context and model is requested - // to make code edits in a single pass. - fullContext: { - type: "boolean", - aliases: ["f"], - description: `Run in full-context editing approach. The model is given the whole code - directory as context and performs changes in one go without acting.`, - }, - }, - }, -); - -// --------------------------------------------------------------------------- -// Global flag handling -// --------------------------------------------------------------------------- - -// Handle 'completion' subcommand before any prompting or API calls -if (cli.input[0] === "completion") { - const shell = cli.input[1] || "bash"; - const scripts: Record = { - bash: `# bash completion for codex -_codex_completion() { - local cur - cur="\${COMP_WORDS[COMP_CWORD]}" - COMPREPLY=( $(compgen -o default -o filenames -- "\${cur}") ) -} -complete -F _codex_completion codex`, - zsh: `# zsh completion for codex -#compdef codex - -_codex() { - _arguments '*:filename:_files' -} -_codex`, - fish: `# fish completion for codex -complete -c codex -a '(__fish_complete_path)' -d 'file path'`, - }; - const script = scripts[shell]; - if (!script) { - // eslint-disable-next-line no-console - console.error(`Unsupported shell: ${shell}`); - process.exit(1); - } - // eslint-disable-next-line no-console - console.log(script); - process.exit(0); -} - -// For --help, show help and exit. -if (cli.flags.help) { - cli.showHelp(); -} - -// For --config, open custom instructions file in editor and exit. -if (cli.flags.config) { - try { - loadConfig(); // Ensures the file is created if it doesn't already exit. - } catch { - // ignore errors - } - - const filePath = INSTRUCTIONS_FILEPATH; - const editor = - process.env["EDITOR"] || (process.platform === "win32" ? "notepad" : "vi"); - spawnSync(editor, [filePath], { stdio: "inherit" }); - process.exit(0); -} - -// --------------------------------------------------------------------------- -// API key handling -// --------------------------------------------------------------------------- - -const fullContextMode = Boolean(cli.flags.fullContext); -let config = loadConfig(undefined, undefined, { - cwd: process.cwd(), - disableProjectDoc: Boolean(cli.flags.noProjectDoc), - projectDocPath: cli.flags.projectDoc, - isFullContext: fullContextMode, -}); - -// `prompt` can be updated later when the user resumes a previous session -// via the `--history` flag. Therefore it must be declared with `let` rather -// than `const`. -let prompt = cli.input[0]; -const model = cli.flags.model ?? config.model; -const imagePaths = cli.flags.image; -const provider = cli.flags.provider ?? config.provider ?? "openai"; - -const client = { - issuer: "https://auth.openai.com", - client_id: "app_EMoamEEZ73f0CkXaXp7hrann", -}; - -let apiKey = ""; -let savedTokens: - | { - id_token?: string; - access_token?: string; - refresh_token: string; - } - | undefined; - -// Try to load existing auth file if present -try { - const home = os.homedir(); - const authDir = path.join(home, ".codex"); - const authFile = path.join(authDir, "auth.json"); - if (fs.existsSync(authFile)) { - const data = JSON.parse(fs.readFileSync(authFile, "utf-8")); - savedTokens = data.tokens; - const lastRefreshTime = data.last_refresh - ? new Date(data.last_refresh).getTime() - : 0; - const expired = Date.now() - lastRefreshTime > 28 * 24 * 60 * 60 * 1000; - if (data.OPENAI_API_KEY && !expired) { - apiKey = data.OPENAI_API_KEY; - } - } -} catch { - // ignore errors -} - -// Get provider-specific API key if not OpenAI -if (provider.toLowerCase() !== "openai") { - const providerInfo = providers[provider.toLowerCase()]; - if (providerInfo) { - const providerApiKey = process.env[providerInfo.envKey]; - if (providerApiKey) { - apiKey = providerApiKey; - } - } -} - -// Only proceed with OpenAI auth flow if: -// 1. Provider is OpenAI and no API key is set, or -// 2. Login flag is explicitly set -if (provider.toLowerCase() === "openai" && !apiKey) { - if (cli.flags.login) { - apiKey = await fetchApiKey(client.issuer, client.client_id); - try { - const home = os.homedir(); - const authDir = path.join(home, ".codex"); - const authFile = path.join(authDir, "auth.json"); - if (fs.existsSync(authFile)) { - const data = JSON.parse(fs.readFileSync(authFile, "utf-8")); - savedTokens = data.tokens; - } - } catch { - /* ignore */ - } - } else { - apiKey = await fetchApiKey(client.issuer, client.client_id); - } -} - -// Ensure the API key is available as an environment variable for legacy code -process.env["OPENAI_API_KEY"] = apiKey; - -// Only attempt credit redemption for OpenAI provider -if (cli.flags.free && provider.toLowerCase() === "openai") { - // eslint-disable-next-line no-console - console.log(`${chalk.bold("codex --free")} attempting to redeem credits...`); - if (!savedTokens?.refresh_token) { - apiKey = await fetchApiKey(client.issuer, client.client_id, true); - // fetchApiKey includes credit redemption as the end of the flow - } else { - await maybeRedeemCredits( - client.issuer, - client.client_id, - savedTokens.refresh_token, - savedTokens.id_token, - ); - } -} - -// Set of providers that don't require API keys -const NO_API_KEY_REQUIRED = new Set(["ollama"]); - -// Skip API key validation for providers that don't require an API key -if (!apiKey && !NO_API_KEY_REQUIRED.has(provider.toLowerCase())) { - // eslint-disable-next-line no-console - console.error( - `\n${chalk.red(`Missing ${provider} API key.`)}\n\n` + - `Set the environment variable ${chalk.bold( - `${provider.toUpperCase()}_API_KEY`, - )} ` + - `and re-run this command.\n` + - `${ - provider.toLowerCase() === "openai" - ? `You can create a key here: ${chalk.bold( - chalk.underline("https://platform.openai.com/account/api-keys"), - )}\n` - : provider.toLowerCase() === "azure" - ? `You can create a ${chalk.bold( - `${provider.toUpperCase()}_OPENAI_API_KEY`, - )} ` + - `in Azure AI Foundry portal at ${chalk.bold(chalk.underline("https://ai.azure.com"))}.\n` - : provider.toLowerCase() === "gemini" - ? `You can create a ${chalk.bold( - `${provider.toUpperCase()}_API_KEY`, - )} ` + `in the ${chalk.bold(`Google AI Studio`)}.\n` - : `You can create a ${chalk.bold( - `${provider.toUpperCase()}_API_KEY`, - )} ` + `in the ${chalk.bold(`${provider}`)} dashboard.\n` - }`, - ); - process.exit(1); -} - -const flagPresent = Object.hasOwn(cli.flags, "disableResponseStorage"); - -const disableResponseStorage = flagPresent - ? Boolean(cli.flags.disableResponseStorage) // value user actually passed - : (config.disableResponseStorage ?? false); // fall back to YAML, default to false - -config = { - apiKey, - ...config, - model: model ?? config.model, - notify: Boolean(cli.flags.notify), - reasoningEffort: - (cli.flags.reasoning as ReasoningEffort | undefined) ?? "medium", - flexMode: cli.flags.flexMode || (config.flexMode ?? false), - provider, - disableResponseStorage, -}; - -// Check for updates after loading config. This is important because we write state file in -// the config dir. -try { - await checkForUpdates(); -} catch { - // ignore -} - -// For --flex-mode, validate and exit if incorrect. -if (config.flexMode) { - const allowedFlexModels = new Set(["o3", "o4-mini"]); - if (!allowedFlexModels.has(config.model)) { - if (cli.flags.flexMode) { - // eslint-disable-next-line no-console - console.error( - `The --flex-mode option is only supported when using the 'o3' or 'o4-mini' models. ` + - `Current model: '${config.model}'.`, - ); - process.exit(1); - } else { - config.flexMode = false; - } - } -} - -if ( - !(await isModelSupportedForResponses(provider, config.model)) && - (!provider || provider.toLowerCase() === "openai") -) { - // eslint-disable-next-line no-console - console.error( - `The model "${config.model}" does not appear in the list of models ` + - `available to your account. Double-check the spelling (use\n` + - ` openai models list\n` + - `to see the full list) or choose another model with the --model flag.`, - ); - process.exit(1); -} - -let rollout: AppRollout | undefined; - -// For --history, show session selector and optionally update prompt or rollout. -if (cli.flags.history) { - const result: { path: string; mode: "view" | "resume" } | null = - await new Promise((resolve) => { - const instance = render( - React.createElement(SessionsOverlay, { - onView: (p: string) => { - instance.unmount(); - resolve({ path: p, mode: "view" }); - }, - onResume: (p: string) => { - instance.unmount(); - resolve({ path: p, mode: "resume" }); - }, - onExit: () => { - instance.unmount(); - resolve(null); - }, - }), - ); - }); - - if (!result) { - process.exit(0); - } - - if (result.mode === "view") { - try { - const content = fs.readFileSync(result.path, "utf-8"); - rollout = JSON.parse(content) as AppRollout; - } catch (error) { - // eslint-disable-next-line no-console - console.error("Error reading session file:", error); - process.exit(1); - } - } else { - prompt = `Resume this session: ${result.path}`; - } -} - -// For --view, optionally load an existing rollout from disk, display it and exit. -if (cli.flags.view) { - const viewPath = cli.flags.view; - const absolutePath = path.isAbsolute(viewPath) - ? viewPath - : path.join(process.cwd(), viewPath); - try { - const content = fs.readFileSync(absolutePath, "utf-8"); - rollout = JSON.parse(content) as AppRollout; - } catch (error) { - // eslint-disable-next-line no-console - console.error("Error reading rollout file:", error); - process.exit(1); - } -} - -// For --fullcontext, run the separate cli entrypoint and exit. -if (fullContextMode) { - await runSinglePass({ - originalPrompt: prompt, - config, - rootPath: process.cwd(), - }); - onExit(); - process.exit(0); -} - -// Ensure that all values in additionalWritableRoots are absolute paths. -const additionalWritableRoots: ReadonlyArray = ( - cli.flags.writableRoot ?? [] -).map((p) => path.resolve(p)); - -// For --quiet, run the cli without user interactions and exit. -if (cli.flags.quiet) { - process.env["CODEX_QUIET_MODE"] = "1"; - if (!prompt || prompt.trim() === "") { - // eslint-disable-next-line no-console - console.error( - 'Quiet mode requires a prompt string, e.g.,: codex -q "Fix bug #123 in the foobar project"', - ); - process.exit(1); - } - - // Determine approval policy for quiet mode based on flags - const quietApprovalPolicy: ApprovalPolicy = - cli.flags.fullAuto || cli.flags.approvalMode === "full-auto" - ? AutoApprovalMode.FULL_AUTO - : cli.flags.autoEdit || cli.flags.approvalMode === "auto-edit" - ? AutoApprovalMode.AUTO_EDIT - : config.approvalMode || AutoApprovalMode.SUGGEST; - - await runQuietMode({ - prompt, - imagePaths: imagePaths || [], - approvalPolicy: quietApprovalPolicy, - additionalWritableRoots, - config, - }); - onExit(); - process.exit(0); -} - -// Default to the "suggest" policy. -// Determine the approval policy to use in interactive mode. -// -// Priority (highest → lowest): -// 1. --fullAuto – run everything automatically in a sandbox. -// 2. --dangerouslyAutoApproveEverything – run everything **without** a sandbox -// or prompts. This is intended for completely trusted environments. Since -// it is more dangerous than --fullAuto we deliberately give it lower -// priority so a user specifying both flags still gets the safer behaviour. -// 3. --autoEdit – automatically approve edits, but prompt for commands. -// 4. config.approvalMode - use the approvalMode setting from ~/.codex/config.json. -// 5. Default – suggest mode (prompt for everything). - -const approvalPolicy: ApprovalPolicy = - cli.flags.fullAuto || cli.flags.approvalMode === "full-auto" - ? AutoApprovalMode.FULL_AUTO - : cli.flags.autoEdit || cli.flags.approvalMode === "auto-edit" - ? AutoApprovalMode.AUTO_EDIT - : config.approvalMode || AutoApprovalMode.SUGGEST; - -const instance = render( - , - { - patchConsole: process.env["DEBUG"] ? false : true, - }, -); -setInkRenderer(instance); - -function formatResponseItemForQuietMode(item: ResponseItem): string { - if (!PRETTY_PRINT) { - return JSON.stringify(item); - } - switch (item.type) { - case "message": { - const role = item.role === "assistant" ? "assistant" : item.role; - const txt = item.content - .map((c) => { - if (c.type === "output_text" || c.type === "input_text") { - return c.text; - } - if (c.type === "input_image") { - return ""; - } - if (c.type === "input_file") { - return c.filename; - } - if (c.type === "refusal") { - return c.refusal; - } - return "?"; - }) - .join(" "); - return `${role}: ${txt}`; - } - case "function_call": { - const details = parseToolCall(item); - return `$ ${details?.cmdReadableText ?? item.name}`; - } - case "function_call_output": { - // @ts-expect-error metadata unknown on ResponseFunctionToolCallOutputItem - const meta = item.metadata as ExecOutputMetadata; - const parts: Array = []; - if (typeof meta?.exit_code === "number") { - parts.push(`code: ${meta.exit_code}`); - } - if (typeof meta?.duration_seconds === "number") { - parts.push(`duration: ${meta.duration_seconds}s`); - } - const header = parts.length > 0 ? ` (${parts.join(", ")})` : ""; - return `command.stdout${header}\n${item.output}`; - } - default: { - return JSON.stringify(item); - } - } -} - -async function runQuietMode({ - prompt, - imagePaths, - approvalPolicy, - additionalWritableRoots, - config, -}: { - prompt: string; - imagePaths: Array; - approvalPolicy: ApprovalPolicy; - additionalWritableRoots: ReadonlyArray; - config: AppConfig; -}): Promise { - const agent = new AgentLoop({ - model: config.model, - config: config, - instructions: config.instructions, - provider: config.provider, - approvalPolicy, - additionalWritableRoots, - disableResponseStorage: config.disableResponseStorage, - onItem: (item: ResponseItem) => { - // eslint-disable-next-line no-console - console.log(formatResponseItemForQuietMode(item)); - }, - onLoading: () => { - /* intentionally ignored in quiet mode */ - }, - getCommandConfirmation: ( - _command: Array, - ): Promise => { - // In quiet mode, default to NO_CONTINUE, except when in full-auto mode - const reviewDecision = - approvalPolicy === AutoApprovalMode.FULL_AUTO - ? ReviewDecision.YES - : ReviewDecision.NO_CONTINUE; - return Promise.resolve({ review: reviewDecision }); - }, - onLastResponseId: () => { - /* intentionally ignored in quiet mode */ - }, - }); - - const inputItem = await createInputItem(prompt, imagePaths); - await agent.run([inputItem]); -} - -const exit = () => { - onExit(); - process.exit(0); -}; - -process.on("SIGINT", exit); -process.on("SIGQUIT", exit); -process.on("SIGTERM", exit); - -// --------------------------------------------------------------------------- -// Fallback for Ctrl-C when stdin is in raw-mode -// --------------------------------------------------------------------------- - -if (process.stdin.isTTY) { - // Ensure we do not leave the terminal in raw mode if the user presses - // Ctrl-C while some other component has focus and Ink is intercepting - // input. Node does *not* emit a SIGINT in raw-mode, so we listen for the - // corresponding byte (0x03) ourselves and trigger a graceful shutdown. - const onRawData = (data: Buffer | string): void => { - const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; - if (str === "\u0003") { - exit(); - } - }; - process.stdin.on("data", onRawData); -} - -// Ensure terminal clean-up always runs, even when other code calls -// `process.exit()` directly. -process.once("exit", onExit); diff --git a/codex-cli/src/components/approval-mode-overlay.tsx b/codex-cli/src/components/approval-mode-overlay.tsx deleted file mode 100644 index dd079e3b2c..0000000000 --- a/codex-cli/src/components/approval-mode-overlay.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import TypeaheadOverlay from "./typeahead-overlay.js"; -import { AutoApprovalMode } from "../utils/auto-approval-mode.js"; -import { Text } from "ink"; -import React from "react"; - -type Props = { - currentMode: string; - onSelect: (mode: string) => void; - onExit: () => void; -}; - -/** - * Overlay to switch between the different automatic‑approval policies. - * - * The list of available modes is derived from the AutoApprovalMode enum so we - * stay in sync with the core agent behaviour. It re‑uses the generic - * TypeaheadOverlay component for the actual UI/UX. - */ -export default function ApprovalModeOverlay({ - currentMode, - onSelect, - onExit, -}: Props): JSX.Element { - const items = React.useMemo( - () => - Object.values(AutoApprovalMode).map((m) => ({ - label: m, - value: m, - })), - [], - ); - - return ( - - Current mode: {currentMode} - - } - initialItems={items} - currentValue={currentMode} - onSelect={onSelect} - onExit={onExit} - /> - ); -} diff --git a/codex-cli/src/components/chat/message-history.tsx b/codex-cli/src/components/chat/message-history.tsx deleted file mode 100644 index bab6b1663f..0000000000 --- a/codex-cli/src/components/chat/message-history.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import type { TerminalHeaderProps } from "./terminal-header.js"; -import type { GroupedResponseItem } from "./use-message-grouping.js"; -import type { ResponseItem } from "openai/resources/responses/responses.mjs"; -import type { FileOpenerScheme } from "src/utils/config.js"; - -import TerminalChatResponseItem from "./terminal-chat-response-item.js"; -import TerminalHeader from "./terminal-header.js"; -import { Box, Static } from "ink"; -import React from "react"; - -// A batch entry can either be a standalone response item or a grouped set of -// items (e.g. auto‑approved tool‑call batches) that should be rendered -// together. -type BatchEntry = { item?: ResponseItem; group?: GroupedResponseItem }; -type MessageHistoryProps = { - batch: Array; - groupCounts: Record; - items: Array; - userMsgCount: number; - confirmationPrompt: React.ReactNode; - loading: boolean; - headerProps: TerminalHeaderProps; - fileOpener: FileOpenerScheme | undefined; -}; - -const MessageHistory: React.FC = ({ - batch, - headerProps, - fileOpener, -}) => { - const messages = batch.map(({ item }) => item!); - - return ( - - {/* - * The Static component receives a mixed array of the literal string - * "header" plus the streamed ResponseItem objects. After filtering out - * the header entry we can safely treat the remaining values as - * ResponseItem, however TypeScript cannot infer the refined type from - * the runtime check and therefore reports property‑access errors. - * - * A short cast after the refinement keeps the implementation tidy while - * preserving type‑safety. - */} - - {(item, index) => { - if (item === "header") { - return ; - } - - // After the guard above `item` can only be a ResponseItem. - const message = item as ResponseItem; - return ( - - - - ); - }} - - - ); -}; - -export default React.memo(MessageHistory); diff --git a/codex-cli/src/components/chat/multiline-editor.tsx b/codex-cli/src/components/chat/multiline-editor.tsx deleted file mode 100644 index 6b24bc27f2..0000000000 --- a/codex-cli/src/components/chat/multiline-editor.tsx +++ /dev/null @@ -1,392 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { useTerminalSize } from "../../hooks/use-terminal-size"; -import TextBuffer from "../../text-buffer.js"; -import chalk from "chalk"; -import { Box, Text, useInput } from "ink"; -import { EventEmitter } from "node:events"; -import React, { useRef, useState } from "react"; - -/* -------------------------------------------------------------------------- - * Polyfill missing `ref()` / `unref()` methods on the mock `Stdin` stream - * provided by `ink-testing-library`. - * - * The real `process.stdin` object exposed by Node.js inherits these methods - * from `Socket`, but the lightweight stub used in tests only extends - * `EventEmitter`. Ink calls the two methods when enabling/disabling raw - * mode, so make them harmless no-ops when they're absent to avoid runtime - * failures during unit tests. - * ----------------------------------------------------------------------- */ - -// Cast through `unknown` ➜ `any` to avoid the `TS2352`/`TS4111` complaints -// when augmenting the prototype with the stubbed `ref`/`unref` methods in the -// test environment. Using `any` here is acceptable because we purposefully -// monkey‑patch internals of Node's `EventEmitter` solely for the benefit of -// Ink's stdin stub – type‑safety is not a primary concern at this boundary. -// -const proto: any = EventEmitter.prototype; - -if (typeof proto["ref"] !== "function") { - proto["ref"] = function ref() {}; -} -if (typeof proto["unref"] !== "function") { - proto["unref"] = function unref() {}; -} - -/* - * The `ink-testing-library` stub emits only a `data` event when its `stdin` - * mock receives `write()` calls. Ink, however, listens for `readable` and - * uses the `read()` method to fetch the buffered chunk. Bridge the gap by - * hooking into `EventEmitter.emit` so that every `data` emission also: - * 1. Buffers the chunk for a subsequent `read()` call, and - * 2. Triggers a `readable` event, matching the contract expected by Ink. - */ - -// Preserve original emit to avoid infinite recursion. -// eslint‑disable‑next‑line @typescript-eslint/no‑unsafe‑assignment -const originalEmit = proto["emit"] as (...args: Array) => boolean; - -proto["emit"] = function patchedEmit( - this: any, - event: string, - ...args: Array -): boolean { - if (event === "data") { - const chunk = args[0] as string; - - if ( - process.env["TEXTBUFFER_DEBUG"] === "1" || - process.env["TEXTBUFFER_DEBUG"] === "true" - ) { - // eslint-disable-next-line no-console - console.log("[MultilineTextEditor:stdin] data", JSON.stringify(chunk)); - } - // Store carriage returns as‑is so that Ink can distinguish between plain - // ("\r") and a bare line‑feed ("\n"). This matters because Ink's - // `parseKeypress` treats "\r" as key.name === "return", whereas "\n" maps - // to "enter" – allowing us to differentiate between plain Enter (submit) - // and Shift+Enter (insert newline) inside `useInput`. - - // Identify the lightweight testing stub: lacks `.read()` but exposes - // `.setRawMode()` and `isTTY` similar to the real TTY stream. - if ( - !(this as any)._inkIsStub && - typeof (this as any).setRawMode === "function" && - typeof (this as any).isTTY === "boolean" && - typeof (this as any).read !== "function" - ) { - (this as any)._inkIsStub = true; - - // Provide a minimal `read()` shim so Ink can pull queued chunks. - (this as any).read = function read() { - const ret = (this as any)._inkBuffered ?? null; - (this as any)._inkBuffered = null; - if ( - process.env["TEXTBUFFER_DEBUG"] === "1" || - process.env["TEXTBUFFER_DEBUG"] === "true" - ) { - // eslint-disable-next-line no-console - console.log("[MultilineTextEditor:stdin.read]", JSON.stringify(ret)); - } - return ret; - }; - } - - if ((this as any)._inkIsStub) { - // Buffer the payload so that `read()` can synchronously retrieve it. - if (typeof (this as any)._inkBuffered === "string") { - (this as any)._inkBuffered += chunk; - } else { - (this as any)._inkBuffered = chunk; - } - - // Notify listeners that data is ready in a way Ink understands. - if ( - process.env["TEXTBUFFER_DEBUG"] === "1" || - process.env["TEXTBUFFER_DEBUG"] === "true" - ) { - // eslint-disable-next-line no-console - console.log( - "[MultilineTextEditor:stdin] -> readable", - JSON.stringify(chunk), - ); - } - originalEmit.call(this, "readable"); - } - } - - // Forward the original event. - return originalEmit.call(this, event, ...args); -}; - -export interface MultilineTextEditorProps { - // Initial contents. - readonly initialText?: string; - - // Visible width. - readonly width?: number; - - // Visible height. - readonly height?: number; - - // Called when the user submits (plain key). - readonly onSubmit?: (text: string) => void; - - // Capture keyboard input. - readonly focus?: boolean; - - // Called when the internal text buffer updates. - readonly onChange?: (text: string) => void; - - // Optional initial cursor position (character offset) - readonly initialCursorOffset?: number; -} - -// Expose a minimal imperative API so parent components (e.g. TerminalChatInput) -// can query the caret position to implement behaviours like history -// navigation that depend on whether the cursor sits on the first/last line. -export interface MultilineTextEditorHandle { - /** Current caret row */ - getRow(): number; - /** Current caret column */ - getCol(): number; - /** Total number of lines in the buffer */ - getLineCount(): number; - /** Helper: caret is on the very first row */ - isCursorAtFirstRow(): boolean; - /** Helper: caret is on the very last row */ - isCursorAtLastRow(): boolean; - /** Full text contents */ - getText(): string; - /** Move the cursor to the end of the text */ - moveCursorToEnd(): void; -} - -const MultilineTextEditorInner = ( - { - initialText = "", - // Width can be provided by the caller. When omitted we fall back to the - // current terminal size (minus some padding handled by `useTerminalSize`). - width, - height = 10, - onSubmit, - focus = true, - onChange, - initialCursorOffset, - }: MultilineTextEditorProps, - ref: React.Ref, -): React.ReactElement => { - // --------------------------------------------------------------------------- - // Editor State - // --------------------------------------------------------------------------- - - const buffer = useRef(new TextBuffer(initialText, initialCursorOffset)); - const [version, setVersion] = useState(0); - - // Keep track of the current terminal size so that the editor grows/shrinks - // with the window. `useTerminalSize` already subtracts a small horizontal - // padding so that we don't butt up right against the edge. - const terminalSize = useTerminalSize(); - - // If the caller didn't specify a width we dynamically choose one based on - // the terminal's current column count. We still enforce a reasonable - // minimum so that the UI never becomes unusably small. - const effectiveWidth = Math.max(20, width ?? terminalSize.columns); - - // --------------------------------------------------------------------------- - // Keyboard handling. - // --------------------------------------------------------------------------- - - useInput( - (input, key) => { - if (!focus) { - return; - } - - if ( - process.env["TEXTBUFFER_DEBUG"] === "1" || - process.env["TEXTBUFFER_DEBUG"] === "true" - ) { - // eslint-disable-next-line no-console - console.log("[MultilineTextEditor] event", { input, key }); - } - - // 1a) CSI-u / modifyOtherKeys *mode 2* (Ink strips initial ESC, so we - // start with '[') – format: "[;u". - if (input.startsWith("[") && input.endsWith("u")) { - const m = input.match(/^\[([0-9]+);([0-9]+)u$/); - if (m && m[1] === "13") { - const mod = Number(m[2]); - // In xterm's encoding: bit-1 (value 2) is Shift. Everything >1 that - // isn't exactly 1 means some modifier was held. We treat *shift or - // alt present* (2,3,4,6,8,9) as newline; Ctrl (bit-2 / value 4) - // triggers submit. See xterm/DEC modifyOtherKeys docs. - - const hasCtrl = Math.floor(mod / 4) % 2 === 1; - if (hasCtrl) { - if (onSubmit) { - onSubmit(buffer.current.getText()); - } - } else { - buffer.current.newline(); - } - setVersion((v) => v + 1); - return; - } - } - - // 1b) CSI-~ / modifyOtherKeys *mode 1* – format: "[27;;~". - // Terminals such as iTerm2 (default), older xterm versions, or when - // modifyOtherKeys=1 is configured, emit this legacy sequence. We - // translate it to the same behaviour as the mode‑2 variant above so - // that Shift+Enter (newline) / Ctrl+Enter (submit) work regardless - // of the user’s terminal settings. - if (input.startsWith("[27;") && input.endsWith("~")) { - const m = input.match(/^\[27;([0-9]+);13~$/); - if (m) { - const mod = Number(m[1]); - const hasCtrl = Math.floor(mod / 4) % 2 === 1; - - if (hasCtrl) { - if (onSubmit) { - onSubmit(buffer.current.getText()); - } - } else { - buffer.current.newline(); - } - setVersion((v) => v + 1); - return; - } - } - - // 2) Single‑byte control chars ------------------------------------------------ - if (input === "\n") { - // Ctrl+J or pasted newline → insert newline. - buffer.current.newline(); - setVersion((v) => v + 1); - return; - } - - if (input === "\r") { - // Plain Enter – submit (works on all basic terminals). - if (onSubmit) { - onSubmit(buffer.current.getText()); - } - return; - } - - // Let fall through so the parent handler (if any) can act on it. - - // Delegate remaining keys to our pure TextBuffer - if ( - process.env["TEXTBUFFER_DEBUG"] === "1" || - process.env["TEXTBUFFER_DEBUG"] === "true" - ) { - // eslint-disable-next-line no-console - console.log("[MultilineTextEditor] key event", { input, key }); - } - - const modified = buffer.current.handleInput( - input, - key as Record, - { height, width: effectiveWidth }, - ); - if (modified) { - setVersion((v) => v + 1); - } - - const newText = buffer.current.getText(); - if (onChange) { - onChange(newText); - } - }, - { isActive: focus }, - ); - - // --------------------------------------------------------------------------- - // Rendering helpers. - // --------------------------------------------------------------------------- - - /* ------------------------------------------------------------------------- */ - /* Imperative handle – expose a read‑only view of caret & buffer geometry */ - /* ------------------------------------------------------------------------- */ - - React.useImperativeHandle( - ref, - () => ({ - getRow: () => buffer.current.getCursor()[0], - getCol: () => buffer.current.getCursor()[1], - getLineCount: () => buffer.current.getText().split("\n").length, - isCursorAtFirstRow: () => buffer.current.getCursor()[0] === 0, - isCursorAtLastRow: () => { - const [row] = buffer.current.getCursor(); - const lineCount = buffer.current.getText().split("\n").length; - return row === lineCount - 1; - }, - getText: () => buffer.current.getText(), - moveCursorToEnd: () => { - buffer.current.move("home"); - const lines = buffer.current.getText().split("\n"); - for (let i = 0; i < lines.length - 1; i++) { - buffer.current.move("down"); - } - buffer.current.move("end"); - // Force a re-render - setVersion((v) => v + 1); - }, - }), - [], - ); - - // Read everything from the buffer - const visibleLines = buffer.current.getVisibleLines({ - height, - width: effectiveWidth, - }); - const [cursorRow, cursorCol] = buffer.current.getCursor(); - const scrollRow = (buffer.current as any).scrollRow as number; - const scrollCol = (buffer.current as any).scrollCol as number; - - return ( - - {visibleLines.map((lineText, idx) => { - const absoluteRow = scrollRow + idx; - - // apply horizontal slice - let display = lineText.slice(scrollCol, scrollCol + effectiveWidth); - if (display.length < effectiveWidth) { - display = display.padEnd(effectiveWidth, " "); - } - - // Highlight the *character under the caret* (i.e. the one immediately - // to the right of the insertion position) so that the block cursor - // visually matches the logical caret location. This makes the - // highlighted glyph the one that would be replaced by `insert()` and - // *not* the one that would be removed by `backspace()`. - - if (absoluteRow === cursorRow) { - const relativeCol = cursorCol - scrollCol; - const highlightCol = relativeCol; - - if (highlightCol >= 0 && highlightCol < effectiveWidth) { - const charToHighlight = display[highlightCol] || " "; - const highlighted = chalk.inverse(charToHighlight); - display = - display.slice(0, highlightCol) + - highlighted + - display.slice(highlightCol + 1); - } else if (relativeCol === effectiveWidth) { - // Caret sits just past the right edge; show a block cursor in the - // gutter so the user still sees it. - display = display.slice(0, effectiveWidth - 1) + chalk.inverse(" "); - } - } - - return {display}; - })} - - ); -}; - -const MultilineTextEditor = React.forwardRef(MultilineTextEditorInner); -export default MultilineTextEditor; diff --git a/codex-cli/src/components/chat/terminal-chat-command-review.tsx b/codex-cli/src/components/chat/terminal-chat-command-review.tsx deleted file mode 100644 index 912af97961..0000000000 --- a/codex-cli/src/components/chat/terminal-chat-command-review.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import { ReviewDecision } from "../../utils/agent/review"; -// TODO: figure out why `cli-spinners` fails on Node v20.9.0 -// which is why we have to do this in the first place -// -// @ts-expect-error select.js is JavaScript and has no types -import { Select } from "../vendor/ink-select/select"; -import TextInput from "../vendor/ink-text-input"; -import { Box, Text, useInput } from "ink"; -import React from "react"; - -// default deny‑reason: -const DEFAULT_DENY_MESSAGE = - "Don't do that, but keep trying to fix the problem"; - -export function TerminalChatCommandReview({ - confirmationPrompt, - onReviewCommand, - // callback to switch approval mode overlay - onSwitchApprovalMode, - explanation: propExplanation, - // whether this review Select is active (listening for keys) - isActive = true, -}: { - confirmationPrompt: React.ReactNode; - onReviewCommand: (decision: ReviewDecision, customMessage?: string) => void; - onSwitchApprovalMode: () => void; - explanation?: string; - // when false, disable the underlying Select so it won't capture input - isActive?: boolean; -}): React.ReactElement { - const [mode, setMode] = React.useState<"select" | "input" | "explanation">( - "select", - ); - const [explanation, setExplanation] = React.useState(""); - - // If the component receives an explanation prop, update the state - React.useEffect(() => { - if (propExplanation) { - setExplanation(propExplanation); - setMode("explanation"); - } - }, [propExplanation]); - const [msg, setMsg] = React.useState(""); - - // ------------------------------------------------------------------------- - // Determine whether the "always approve" option should be displayed. We - // only hide it for the special `apply_patch` command since approving those - // permanently would bypass the user's review of future file modifications. - // The information is embedded in the `confirmationPrompt` React element – - // we inspect the `commandForDisplay` prop exposed by - // to extract the base command. - // ------------------------------------------------------------------------- - - const showAlwaysApprove = React.useMemo(() => { - if ( - React.isValidElement(confirmationPrompt) && - // eslint-disable-next-line @typescript-eslint/no-explicit-any - typeof (confirmationPrompt as any).props?.commandForDisplay === "string" - ) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const command: string = (confirmationPrompt as any).props - .commandForDisplay; - // Grab the first token of the first line – that corresponds to the base - // command even when the string contains embedded newlines (e.g. diffs). - const baseCmd = command.split("\n")[0]?.trim().split(/\s+/)[0] ?? ""; - return baseCmd !== "apply_patch"; - } - // Default to showing the option when we cannot reliably detect the base - // command. - return true; - }, [confirmationPrompt]); - - // Memoize the list of selectable options to avoid recreating the array on - // every render. This keeps { - if (value === "edit") { - setMode("input"); - } else if (value === "switch") { - onSwitchApprovalMode(); - } else { - onReviewCommand(value); - } - }} - options={approvalOptions} - /> - - - ) : mode === "input" ? ( - <> - Give the model feedback (↵ to submit): - - - - - - - {msg.trim() === "" && ( - - - default:  - {DEFAULT_DENY_MESSAGE} - - - )} - - ) : null} - - - ); -} diff --git a/codex-cli/src/components/chat/terminal-chat-completions.tsx b/codex-cli/src/components/chat/terminal-chat-completions.tsx deleted file mode 100644 index eb7e47f85f..0000000000 --- a/codex-cli/src/components/chat/terminal-chat-completions.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Box, Text } from "ink"; -import React, { useMemo } from "react"; - -type TextCompletionProps = { - /** - * Array of text completion options to display in the list - */ - completions: Array; - - /** - * Maximum number of completion items to show at once in the view - */ - displayLimit: number; - - /** - * Index of the currently selected completion in the completions array - */ - selectedCompletion: number; -}; - -function TerminalChatCompletions({ - completions, - selectedCompletion, - displayLimit, -}: TextCompletionProps): JSX.Element { - const visibleItems = useMemo(() => { - // Try to keep selection centered in view - let startIndex = Math.max( - 0, - selectedCompletion - Math.floor(displayLimit / 2), - ); - - // Fix window position when at the end of the list - if (completions.length - startIndex < displayLimit) { - startIndex = Math.max(0, completions.length - displayLimit); - } - - const endIndex = Math.min(completions.length, startIndex + displayLimit); - - return completions.slice(startIndex, endIndex).map((completion, index) => ({ - completion, - originalIndex: index + startIndex, - })); - }, [completions, selectedCompletion, displayLimit]); - - return ( - - {visibleItems.map(({ completion, originalIndex }) => ( - - {completion} - - ))} - - ); -} - -export default TerminalChatCompletions; diff --git a/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx b/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx deleted file mode 100644 index 714cc59fb3..0000000000 --- a/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { log } from "../../utils/logger/log.js"; -import { Box, Text, useInput, useStdin } from "ink"; -import React, { useState } from "react"; -import { useInterval } from "use-interval"; - -// Retaining a single static placeholder text for potential future use. The -// more elaborate randomised thinking prompts were removed to streamline the -// UI – the elapsed‑time counter now provides sufficient feedback. - -export default function TerminalChatInputThinking({ - onInterrupt, - active, - thinkingSeconds, -}: { - onInterrupt: () => void; - active: boolean; - thinkingSeconds: number; -}): React.ReactElement { - const [awaitingConfirm, setAwaitingConfirm] = useState(false); - const [dots, setDots] = useState(""); - - // Animate the ellipsis - useInterval(() => { - setDots((prev) => (prev.length < 3 ? prev + "." : "")); - }, 500); - - const { stdin, setRawMode } = useStdin(); - - React.useEffect(() => { - if (!active) { - return; - } - - setRawMode?.(true); - - const onData = (data: Buffer | string) => { - if (awaitingConfirm) { - return; - } - - const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; - if (str === "\x1b\x1b") { - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - setAwaitingConfirm(true); - setTimeout(() => setAwaitingConfirm(false), 1500); - } - }; - - stdin?.on("data", onData); - return () => { - stdin?.off("data", onData); - }; - }, [stdin, awaitingConfirm, onInterrupt, active, setRawMode]); - - // No timers required beyond tracking the elapsed seconds supplied via props. - - useInput( - (_input, key) => { - if (!key.escape) { - return; - } - - if (awaitingConfirm) { - log("useInput: second ESC detected – triggering onInterrupt()"); - onInterrupt(); - setAwaitingConfirm(false); - } else { - log("useInput: first ESC detected – waiting for confirmation"); - setAwaitingConfirm(true); - setTimeout(() => setAwaitingConfirm(false), 1500); - } - }, - { isActive: active }, - ); - - // Custom ball animation including the elapsed seconds - const ballFrames = [ - "( ● )", - "( ● )", - "( ● )", - "( ● )", - "( ●)", - "( ● )", - "( ● )", - "( ● )", - "( ● )", - "(● )", - ]; - - const [frame, setFrame] = useState(0); - - useInterval(() => { - setFrame((idx) => (idx + 1) % ballFrames.length); - }, 80); - - // Preserve the spinner (ball) animation while keeping the elapsed seconds - // text static. We achieve this by rendering the bouncing ball inside the - // parentheses and appending the seconds counter *after* the spinner rather - // than injecting it directly next to the ball (which caused the counter to - // move horizontally together with the ball). - - const frameTemplate = ballFrames[frame] ?? ballFrames[0]; - const frameWithSeconds = `${frameTemplate} ${thinkingSeconds}s`; - - return ( - - - - {frameWithSeconds} - - Thinking - {dots} - - - - Press Esc twice to interrupt - - - {awaitingConfirm && ( - - Press Esc again to interrupt and enter a new - instruction - - )} - - ); -} diff --git a/codex-cli/src/components/chat/terminal-chat-input.tsx b/codex-cli/src/components/chat/terminal-chat-input.tsx deleted file mode 100644 index 66428f8463..0000000000 --- a/codex-cli/src/components/chat/terminal-chat-input.tsx +++ /dev/null @@ -1,1017 +0,0 @@ -import type { MultilineTextEditorHandle } from "./multiline-editor"; -import type { ReviewDecision } from "../../utils/agent/review.js"; -import type { FileSystemSuggestion } from "../../utils/file-system-suggestions.js"; -import type { HistoryEntry } from "../../utils/storage/command-history.js"; -import type { - ResponseInputItem, - ResponseItem, -} from "openai/resources/responses/responses.mjs"; - -import MultilineTextEditor from "./multiline-editor"; -import { TerminalChatCommandReview } from "./terminal-chat-command-review.js"; -import TextCompletions from "./terminal-chat-completions.js"; -import { loadConfig } from "../../utils/config.js"; -import { getFileSystemSuggestions } from "../../utils/file-system-suggestions.js"; -import { expandFileTags } from "../../utils/file-tag-utils"; -import { createInputItem } from "../../utils/input-utils.js"; -import { log } from "../../utils/logger/log.js"; -import { setSessionId } from "../../utils/session.js"; -import { SLASH_COMMANDS, type SlashCommand } from "../../utils/slash-commands"; -import { - loadCommandHistory, - addToHistory, -} from "../../utils/storage/command-history.js"; -import { clearTerminal, onExit } from "../../utils/terminal.js"; -import { Box, Text, useApp, useInput, useStdin } from "ink"; -import { fileURLToPath } from "node:url"; -import React, { - useCallback, - useState, - Fragment, - useEffect, - useRef, -} from "react"; -import { useInterval } from "use-interval"; - -const suggestions = [ - "explain this codebase to me", - "fix any build errors", - "are there any bugs in my code?", -]; - -export default function TerminalChatInput({ - isNew, - loading, - submitInput, - confirmationPrompt, - explanation, - submitConfirmation, - setLastResponseId, - setItems, - contextLeftPercent, - openOverlay, - openModelOverlay, - openApprovalOverlay, - openHelpOverlay, - openDiffOverlay, - openSessionsOverlay, - onCompact, - interruptAgent, - active, - thinkingSeconds, - items = [], -}: { - isNew: boolean; - loading: boolean; - submitInput: (input: Array) => void; - confirmationPrompt: React.ReactNode | null; - explanation?: string; - submitConfirmation: ( - decision: ReviewDecision, - customDenyMessage?: string, - ) => void; - setLastResponseId: (lastResponseId: string) => void; - setItems: React.Dispatch>>; - contextLeftPercent: number; - openOverlay: () => void; - openModelOverlay: () => void; - openApprovalOverlay: () => void; - openHelpOverlay: () => void; - openDiffOverlay: () => void; - openSessionsOverlay: () => void; - onCompact: () => void; - interruptAgent: () => void; - active: boolean; - thinkingSeconds: number; - // New: current conversation items so we can include them in bug reports - items?: Array; -}): React.ReactElement { - // Slash command suggestion index - const [selectedSlashSuggestion, setSelectedSlashSuggestion] = - useState(0); - const app = useApp(); - const [selectedSuggestion, setSelectedSuggestion] = useState(0); - const [input, setInput] = useState(""); - const [history, setHistory] = useState>([]); - const [historyIndex, setHistoryIndex] = useState(null); - const [draftInput, setDraftInput] = useState(""); - const [skipNextSubmit, setSkipNextSubmit] = useState(false); - const [fsSuggestions, setFsSuggestions] = useState< - Array - >([]); - const [selectedCompletion, setSelectedCompletion] = useState(-1); - // Multiline text editor key to force remount after submission - const [editorState, setEditorState] = useState<{ - key: number; - initialCursorOffset?: number; - }>({ key: 0 }); - // Imperative handle from the multiline editor so we can query caret position - const editorRef = useRef(null); - // Track the caret row across keystrokes - const prevCursorRow = useRef(null); - const prevCursorWasAtLastRow = useRef(false); - - // --- Helper for updating input, remounting editor, and moving cursor to end --- - const applyFsSuggestion = useCallback((newInputText: string) => { - setInput(newInputText); - setEditorState((s) => ({ - key: s.key + 1, - initialCursorOffset: newInputText.length, - })); - }, []); - - // --- Helper for updating file system suggestions --- - function updateFsSuggestions( - txt: string, - alwaysUpdateSelection: boolean = false, - ) { - // Clear file system completions if a space is typed - if (txt.endsWith(" ")) { - setFsSuggestions([]); - setSelectedCompletion(-1); - } else { - // Determine the current token (last whitespace-separated word) - const words = txt.trim().split(/\s+/); - const lastWord = words[words.length - 1] ?? ""; - - const shouldUpdateSelection = - lastWord.startsWith("@") || alwaysUpdateSelection; - - // Strip optional leading '@' for the path prefix - let pathPrefix: string; - if (lastWord.startsWith("@")) { - pathPrefix = lastWord.slice(1); - // If only '@' is typed, list everything in the current directory - pathPrefix = pathPrefix.length === 0 ? "./" : pathPrefix; - } else { - pathPrefix = lastWord; - } - - if (shouldUpdateSelection) { - const completions = getFileSystemSuggestions(pathPrefix); - setFsSuggestions(completions); - if (completions.length > 0) { - setSelectedCompletion((prev) => - prev < 0 || prev >= completions.length ? 0 : prev, - ); - } else { - setSelectedCompletion(-1); - } - } else if (fsSuggestions.length > 0) { - // Token cleared → clear menu - setFsSuggestions([]); - setSelectedCompletion(-1); - } - } - } - - /** - * Result of replacing text with a file system suggestion - */ - interface ReplacementResult { - /** The new text with the suggestion applied */ - text: string; - /** The selected suggestion if a replacement was made */ - suggestion: FileSystemSuggestion | null; - /** Whether a replacement was actually made */ - wasReplaced: boolean; - } - - // --- Helper for replacing input with file system suggestion --- - function getFileSystemSuggestion( - txt: string, - requireAtPrefix: boolean = false, - ): ReplacementResult { - if (fsSuggestions.length === 0 || selectedCompletion < 0) { - return { text: txt, suggestion: null, wasReplaced: false }; - } - - const words = txt.trim().split(/\s+/); - const lastWord = words[words.length - 1] ?? ""; - - // Check if @ prefix is required and the last word doesn't have it - if (requireAtPrefix && !lastWord.startsWith("@")) { - return { text: txt, suggestion: null, wasReplaced: false }; - } - - const selected = fsSuggestions[selectedCompletion]; - if (!selected) { - return { text: txt, suggestion: null, wasReplaced: false }; - } - - const replacement = lastWord.startsWith("@") - ? `@${selected.path}` - : selected.path; - words[words.length - 1] = replacement; - return { - text: words.join(" "), - suggestion: selected, - wasReplaced: true, - }; - } - - // Load command history on component mount - useEffect(() => { - async function loadHistory() { - const historyEntries = await loadCommandHistory(); - setHistory(historyEntries); - } - - loadHistory(); - }, []); - // Reset slash suggestion index when input prefix changes - useEffect(() => { - if (input.trim().startsWith("/")) { - setSelectedSlashSuggestion(0); - } - }, [input]); - - useInput( - (_input, _key) => { - // Slash command navigation: up/down to select, enter to fill - if (!confirmationPrompt && !loading && input.trim().startsWith("/")) { - const prefix = input.trim(); - const matches = SLASH_COMMANDS.filter((cmd: SlashCommand) => - cmd.command.startsWith(prefix), - ); - if (matches.length > 0) { - if (_key.tab) { - // Cycle and fill slash command suggestions on Tab - const len = matches.length; - // Determine new index based on shift state - const nextIdx = _key.shift - ? selectedSlashSuggestion <= 0 - ? len - 1 - : selectedSlashSuggestion - 1 - : selectedSlashSuggestion >= len - 1 - ? 0 - : selectedSlashSuggestion + 1; - setSelectedSlashSuggestion(nextIdx); - // Autocomplete the command in the input - const match = matches[nextIdx]; - if (!match) { - return; - } - const cmd = match.command; - setInput(cmd); - setDraftInput(cmd); - return; - } - if (_key.upArrow) { - setSelectedSlashSuggestion((prev) => - prev <= 0 ? matches.length - 1 : prev - 1, - ); - return; - } - if (_key.downArrow) { - setSelectedSlashSuggestion((prev) => - prev < 0 || prev >= matches.length - 1 ? 0 : prev + 1, - ); - return; - } - if (_key.return) { - // Execute the currently selected slash command - const selIdx = selectedSlashSuggestion; - const cmdObj = matches[selIdx]; - if (cmdObj) { - const cmd = cmdObj.command; - setInput(""); - setDraftInput(""); - setSelectedSlashSuggestion(0); - switch (cmd) { - case "/history": - openOverlay(); - break; - case "/sessions": - openSessionsOverlay(); - break; - case "/help": - openHelpOverlay(); - break; - case "/compact": - onCompact(); - break; - case "/model": - openModelOverlay(); - break; - case "/approval": - openApprovalOverlay(); - break; - case "/diff": - openDiffOverlay(); - break; - case "/bug": - onSubmit(cmd); - break; - case "/clear": - onSubmit(cmd); - break; - case "/clearhistory": - onSubmit(cmd); - break; - default: - break; - } - } - return; - } - } - } - if (!confirmationPrompt && !loading) { - if (fsSuggestions.length > 0) { - if (_key.upArrow) { - setSelectedCompletion((prev) => - prev <= 0 ? fsSuggestions.length - 1 : prev - 1, - ); - return; - } - - if (_key.downArrow) { - setSelectedCompletion((prev) => - prev >= fsSuggestions.length - 1 ? 0 : prev + 1, - ); - return; - } - - if (_key.tab && selectedCompletion >= 0) { - const { text: newText, wasReplaced } = - getFileSystemSuggestion(input); - - // Only proceed if the text was actually changed - if (wasReplaced) { - applyFsSuggestion(newText); - setFsSuggestions([]); - setSelectedCompletion(-1); - } - return; - } - } - - if (_key.upArrow) { - let moveThroughHistory = true; - - // Only use history when the caret was *already* on the very first - // row *before* this key-press. - const cursorRow = editorRef.current?.getRow?.() ?? 0; - const cursorCol = editorRef.current?.getCol?.() ?? 0; - const wasAtFirstRow = (prevCursorRow.current ?? cursorRow) === 0; - if (!(cursorRow === 0 && wasAtFirstRow)) { - moveThroughHistory = false; - } - - // If we are not yet in history mode, then also require that the col is zero so that - // we only trigger history navigation when the user is at the start of the input. - if (historyIndex == null && !(cursorRow === 0 && cursorCol === 0)) { - moveThroughHistory = false; - } - - // Move through history. - if (history.length && moveThroughHistory) { - let newIndex: number; - if (historyIndex == null) { - const currentDraft = editorRef.current?.getText?.() ?? input; - setDraftInput(currentDraft); - newIndex = history.length - 1; - } else { - newIndex = Math.max(0, historyIndex - 1); - } - setHistoryIndex(newIndex); - - setInput(history[newIndex]?.command ?? ""); - // Re-mount the editor so it picks up the new initialText - setEditorState((s) => ({ key: s.key + 1 })); - return; // handled - } - - // Otherwise let it propagate. - } - - if (_key.downArrow) { - // Only move forward in history when we're already *in* history mode - // AND the caret sits on the last line of the buffer. - const wasAtLastRow = - prevCursorWasAtLastRow.current ?? - editorRef.current?.isCursorAtLastRow() ?? - true; - if (historyIndex != null && wasAtLastRow) { - const newIndex = historyIndex + 1; - if (newIndex >= history.length) { - setHistoryIndex(null); - setInput(draftInput); - setEditorState((s) => ({ key: s.key + 1 })); - } else { - setHistoryIndex(newIndex); - setInput(history[newIndex]?.command ?? ""); - setEditorState((s) => ({ key: s.key + 1 })); - } - return; // handled - } - // Otherwise let it propagate - } - - // Defer filesystem suggestion logic to onSubmit if enter key is pressed - if (!_key.return) { - // Pressing tab should trigger the file system suggestions - const shouldUpdateSelection = _key.tab; - const targetInput = _key.delete ? input.slice(0, -1) : input + _input; - updateFsSuggestions(targetInput, shouldUpdateSelection); - } - } - - // Update the cached cursor position *after* **all** handlers (including - // the internal ) have processed this key event. - // - // Ink invokes `useInput` callbacks starting with **parent** components - // first, followed by their descendants. As a result the call above - // executes *before* the editor has had a chance to react to the key - // press and update its internal caret position. When navigating - // through a multi-line draft with the ↑ / ↓ arrow keys this meant we - // recorded the *old* cursor row instead of the one that results *after* - // the key press. Consequently, a subsequent ↑ still saw - // `prevCursorRow = 1` even though the caret was already on row 0 and - // history-navigation never kicked in. - // - // Defer the sampling by one tick so we read the *final* caret position - // for this frame. - setTimeout(() => { - prevCursorRow.current = editorRef.current?.getRow?.() ?? null; - prevCursorWasAtLastRow.current = - editorRef.current?.isCursorAtLastRow?.() ?? true; - }, 1); - - if (input.trim() === "" && isNew) { - if (_key.tab) { - setSelectedSuggestion( - (s) => (s + (_key.shift ? -1 : 1)) % (suggestions.length + 1), - ); - } else if (selectedSuggestion && _key.return) { - const suggestion = suggestions[selectedSuggestion - 1] || ""; - setInput(""); - setSelectedSuggestion(0); - submitInput([ - { - role: "user", - content: [{ type: "input_text", text: suggestion }], - type: "message", - }, - ]); - } - } else if (_input === "\u0003" || (_input === "c" && _key.ctrl)) { - setTimeout(() => { - app.exit(); - onExit(); - process.exit(0); - }, 60); - } - }, - { isActive: active }, - ); - - const onSubmit = useCallback( - async (value: string) => { - const inputValue = value.trim(); - - // If the user only entered a slash, do not send a chat message. - if (inputValue === "/") { - setInput(""); - return; - } - - // Skip this submit if we just autocompleted a slash command. - if (skipNextSubmit) { - setSkipNextSubmit(false); - return; - } - - if (!inputValue) { - return; - } else if (inputValue === "/history") { - setInput(""); - openOverlay(); - return; - } else if (inputValue === "/sessions") { - setInput(""); - openSessionsOverlay(); - return; - } else if (inputValue === "/help") { - setInput(""); - openHelpOverlay(); - return; - } else if (inputValue === "/diff") { - setInput(""); - openDiffOverlay(); - return; - } else if (inputValue === "/compact") { - setInput(""); - onCompact(); - return; - } else if (inputValue.startsWith("/model")) { - setInput(""); - openModelOverlay(); - return; - } else if (inputValue.startsWith("/approval")) { - setInput(""); - openApprovalOverlay(); - return; - } else if (["exit", "q", ":q"].includes(inputValue)) { - setInput(""); - setTimeout(() => { - app.exit(); - onExit(); - process.exit(0); - }, 60); // Wait one frame. - return; - } else if (inputValue === "/clear" || inputValue === "clear") { - setInput(""); - setSessionId(""); - setLastResponseId(""); - - // Clear the terminal screen (including scrollback) before resetting context. - clearTerminal(); - - // Emit a system message to confirm the clear action. We *append* - // it so Ink's treats it as new output and actually renders it. - setItems((prev) => { - const filteredOldItems = prev.filter((item) => { - // Remove any token‑heavy entries (user/assistant turns and function calls) - if ( - item.type === "message" && - (item.role === "user" || item.role === "assistant") - ) { - return false; - } - if ( - item.type === "function_call" || - item.type === "function_call_output" - ) { - return false; - } - return true; // keep developer/system and other meta entries - }); - - return [ - ...filteredOldItems, - { - id: `clear-${Date.now()}`, - type: "message", - role: "system", - content: [{ type: "input_text", text: "Terminal cleared" }], - }, - ]; - }); - - return; - } else if (inputValue === "/clearhistory") { - setInput(""); - - // Import clearCommandHistory function to avoid circular dependencies - // Using dynamic import to lazy-load the function - import("../../utils/storage/command-history.js").then( - async ({ clearCommandHistory }) => { - await clearCommandHistory(); - setHistory([]); - - // Emit a system message to confirm the history clear action. - setItems((prev) => [ - ...prev, - { - id: `clearhistory-${Date.now()}`, - type: "message", - role: "system", - content: [ - { type: "input_text", text: "Command history cleared" }, - ], - }, - ]); - }, - ); - - return; - } else if (inputValue === "/bug") { - // Generate a GitHub bug report URL pre‑filled with session details. - setInput(""); - - try { - const os = await import("node:os"); - const { CLI_VERSION } = await import("../../version.js"); - const { buildBugReportUrl } = await import( - "../../utils/bug-report.js" - ); - - const url = buildBugReportUrl({ - items: items ?? [], - cliVersion: CLI_VERSION, - model: loadConfig().model ?? "unknown", - platform: [os.platform(), os.arch(), os.release()] - .map((s) => `\`${s}\``) - .join(" | "), - }); - - setItems((prev) => [ - ...prev, - { - id: `bugreport-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: `🔗 Bug report URL: ${url}`, - }, - ], - }, - ]); - } catch (error) { - // If anything went wrong, notify the user. - setItems((prev) => [ - ...prev, - { - id: `bugreport-error-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: `⚠️ Failed to create bug report URL: ${error}`, - }, - ], - }, - ]); - } - - return; - } else if (inputValue.startsWith("/")) { - // Handle invalid/unrecognized commands. Only single-word inputs starting with '/' - // (e.g., /command) that are not recognized are caught here. Any other input, including - // those starting with '/' but containing spaces (e.g., "/command arg"), will fall through - // and be treated as a regular prompt. - const trimmed = inputValue.trim(); - - if (/^\/\S+$/.test(trimmed)) { - setInput(""); - setItems((prev) => [ - ...prev, - { - id: `invalidcommand-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: `Invalid command "${trimmed}". Use /help to retrieve the list of commands.`, - }, - ], - }, - ]); - - return; - } - } - - // detect image file paths for dynamic inclusion - const images: Array = []; - let text = inputValue; - - // markdown-style image syntax: ![alt](path) - text = text.replace(/!\[[^\]]*?\]\(([^)]+)\)/g, (_m, p1: string) => { - images.push(p1.startsWith("file://") ? fileURLToPath(p1) : p1); - return ""; - }); - - // quoted file paths ending with common image extensions (e.g. '/path/to/img.png') - text = text.replace( - /['"]([^'"]+?\.(?:png|jpe?g|gif|bmp|webp|svg))['"]/gi, - (_m, p1: string) => { - images.push(p1.startsWith("file://") ? fileURLToPath(p1) : p1); - return ""; - }, - ); - - // bare file paths ending with common image extensions - text = text.replace( - // eslint-disable-next-line no-useless-escape - /\b(?:\.[\/\\]|[\/\\]|[A-Za-z]:[\/\\])?[\w-]+(?:[\/\\][\w-]+)*\.(?:png|jpe?g|gif|bmp|webp|svg)\b/gi, - (match: string) => { - images.push( - match.startsWith("file://") ? fileURLToPath(match) : match, - ); - return ""; - }, - ); - text = text.trim(); - - // Expand @file tokens into XML blocks for the model - const expandedText = await expandFileTags(text); - - const inputItem = await createInputItem(expandedText, images); - submitInput([inputItem]); - - // Get config for history persistence. - const config = loadConfig(); - - // Add to history and update state. - const updatedHistory = await addToHistory(value, history, { - maxSize: config.history?.maxSize ?? 1000, - saveHistory: config.history?.saveHistory ?? true, - sensitivePatterns: config.history?.sensitivePatterns ?? [], - }); - - setHistory(updatedHistory); - setHistoryIndex(null); - setDraftInput(""); - setSelectedSuggestion(0); - setInput(""); - setFsSuggestions([]); - setSelectedCompletion(-1); - }, - [ - setInput, - submitInput, - setLastResponseId, - setItems, - app, - setHistory, - setHistoryIndex, - openOverlay, - openApprovalOverlay, - openModelOverlay, - openHelpOverlay, - openDiffOverlay, - openSessionsOverlay, - history, - onCompact, - skipNextSubmit, - items, - ], - ); - - if (confirmationPrompt) { - return ( - - ); - } - - return ( - - - {loading ? ( - - ) : ( - - { - setDraftInput(txt); - if (historyIndex != null) { - setHistoryIndex(null); - } - setInput(txt); - }} - key={editorState.key} - initialCursorOffset={editorState.initialCursorOffset} - initialText={input} - height={6} - focus={active} - onSubmit={(txt) => { - // If final token is an @path, replace with filesystem suggestion if available - const { - text: replacedText, - suggestion, - wasReplaced, - } = getFileSystemSuggestion(txt, true); - - // If we replaced @path token with a directory, don't submit - if (wasReplaced && suggestion?.isDirectory) { - applyFsSuggestion(replacedText); - // Update suggestions for the new directory - updateFsSuggestions(replacedText, true); - return; - } - - onSubmit(replacedText); - setEditorState((s) => ({ key: s.key + 1 })); - setInput(""); - setHistoryIndex(null); - setDraftInput(""); - }} - /> - - )} - - {/* Slash command autocomplete suggestions */} - {input.trim().startsWith("/") && ( - - {SLASH_COMMANDS.filter((cmd: SlashCommand) => - cmd.command.startsWith(input.trim()), - ).map((cmd: SlashCommand, idx: number) => ( - - - {cmd.command} - {cmd.description} - - - ))} - - )} - - {isNew && !input ? ( - - try:{" "} - {suggestions.map((m, key) => ( - - {key !== 0 ? " | " : ""} - - {m} - - - ))} - - ) : fsSuggestions.length > 0 ? ( - suggestion.path)} - selectedCompletion={selectedCompletion} - displayLimit={5} - /> - ) : ( - - Ctrl+C to exit | "/" to see commands | Enter to send - {contextLeftPercent > 25 && ( - <> - {" — "} - 40 ? "green" : "yellow"}> - {Math.round(contextLeftPercent)}% context left - - - )} - {contextLeftPercent <= 25 && ( - <> - {" — "} - - {Math.round(contextLeftPercent)}% context left — send - "/compact" to condense context - - - )} - - )} - - - ); -} - -function TerminalChatInputThinking({ - onInterrupt, - active, - thinkingSeconds, -}: { - onInterrupt: () => void; - active: boolean; - thinkingSeconds: number; -}) { - const [awaitingConfirm, setAwaitingConfirm] = useState(false); - const [dots, setDots] = useState(""); - - // Animate ellipsis - useInterval(() => { - setDots((prev) => (prev.length < 3 ? prev + "." : "")); - }, 500); - - // Spinner frames with embedded seconds - const ballFrames = [ - "( ● )", - "( ● )", - "( ● )", - "( ● )", - "( ●)", - "( ● )", - "( ● )", - "( ● )", - "( ● )", - "(● )", - ]; - const [frame, setFrame] = useState(0); - - useInterval(() => { - setFrame((idx) => (idx + 1) % ballFrames.length); - }, 80); - - // Keep the elapsed‑seconds text fixed while the ball animation moves. - const frameTemplate = ballFrames[frame] ?? ballFrames[0]; - const frameWithSeconds = `${frameTemplate} ${thinkingSeconds}s`; - - // --------------------------------------------------------------------- - // Raw stdin listener to catch the case where the terminal delivers two - // consecutive ESC bytes ("\x1B\x1B") in a *single* chunk. Ink's `useInput` - // collapses that sequence into one key event, so the regular two‑step - // handler above never sees the second press. By inspecting the raw data - // we can identify this special case and trigger the interrupt while still - // requiring a double press for the normal single‑byte ESC events. - // --------------------------------------------------------------------- - - const { stdin, setRawMode } = useStdin(); - - React.useEffect(() => { - if (!active) { - return; - } - - // Ensure raw mode – already enabled by Ink when the component has focus, - // but called defensively in case that assumption ever changes. - setRawMode?.(true); - - const onData = (data: Buffer | string) => { - if (awaitingConfirm) { - return; // already awaiting a second explicit press - } - - // Handle both Buffer and string forms. - const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; - if (str === "\x1b\x1b") { - // Treat as the first Escape press – prompt the user for confirmation. - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - setAwaitingConfirm(true); - setTimeout(() => setAwaitingConfirm(false), 1500); - } - }; - - stdin?.on("data", onData); - - return () => { - stdin?.off("data", onData); - }; - }, [stdin, awaitingConfirm, onInterrupt, active, setRawMode]); - - // No local timer: the parent component supplies the elapsed time via props. - - // Listen for the escape key to allow the user to interrupt the current - // operation. We require two presses within a short window (1.5s) to avoid - // accidental cancellations. - useInput( - (_input, key) => { - if (!key.escape) { - return; - } - - if (awaitingConfirm) { - log("useInput: second ESC detected – triggering onInterrupt()"); - onInterrupt(); - setAwaitingConfirm(false); - } else { - log("useInput: first ESC detected – waiting for confirmation"); - setAwaitingConfirm(true); - setTimeout(() => setAwaitingConfirm(false), 1500); - } - }, - { isActive: active }, - ); - - return ( - - - - {frameWithSeconds} - - Thinking - {dots} - - - - press Esc{" "} - {awaitingConfirm ? ( - again - ) : ( - twice - )}{" "} - to interrupt - - - - ); -} diff --git a/codex-cli/src/components/chat/terminal-chat-past-rollout.tsx b/codex-cli/src/components/chat/terminal-chat-past-rollout.tsx deleted file mode 100644 index 1ac8280edb..0000000000 --- a/codex-cli/src/components/chat/terminal-chat-past-rollout.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import type { TerminalChatSession } from "../../utils/session.js"; -import type { ResponseItem } from "openai/resources/responses/responses"; -import type { FileOpenerScheme } from "src/utils/config.js"; - -import TerminalChatResponseItem from "./terminal-chat-response-item"; -import { Box, Text } from "ink"; -import React from "react"; - -export default function TerminalChatPastRollout({ - session, - items, - fileOpener, -}: { - session: TerminalChatSession; - items: Array; - fileOpener: FileOpenerScheme | undefined; -}): React.ReactElement { - const { version, id: sessionId, model } = session; - return ( - - - - ● OpenAI Codex{" "} - - (research preview) v{version} - - - - - - localhost{" "} - · session:{" "} - - {sessionId} - - - - When / Who:{" "} - - {session.timestamp} / {session.user} - - - - model: {model} - - - - {React.useMemo( - () => - items.map((item, key) => ( - - )), - [items, fileOpener], - )} - - - ); -} diff --git a/codex-cli/src/components/chat/terminal-chat-response-item.tsx b/codex-cli/src/components/chat/terminal-chat-response-item.tsx deleted file mode 100644 index bab4aa317f..0000000000 --- a/codex-cli/src/components/chat/terminal-chat-response-item.tsx +++ /dev/null @@ -1,360 +0,0 @@ -import type { OverlayModeType } from "./terminal-chat"; -import type { TerminalRendererOptions } from "marked-terminal"; -import type { - ResponseFunctionToolCallItem, - ResponseFunctionToolCallOutputItem, - ResponseInputMessageItem, - ResponseItem, - ResponseOutputMessage, - ResponseReasoningItem, -} from "openai/resources/responses/responses"; -import type { FileOpenerScheme } from "src/utils/config"; - -import { useTerminalSize } from "../../hooks/use-terminal-size"; -import { collapseXmlBlocks } from "../../utils/file-tag-utils"; -import { parseToolCall, parseToolCallOutput } from "../../utils/parsers"; -import chalk, { type ForegroundColorName } from "chalk"; -import { Box, Text } from "ink"; -import { parse, setOptions } from "marked"; -import TerminalRenderer from "marked-terminal"; -import path from "path"; -import React, { useEffect, useMemo } from "react"; -import { formatCommandForDisplay } from "src/format-command.js"; -import supportsHyperlinks from "supports-hyperlinks"; - -export default function TerminalChatResponseItem({ - item, - fullStdout = false, - setOverlayMode, - fileOpener, -}: { - item: ResponseItem; - fullStdout?: boolean; - setOverlayMode?: React.Dispatch>; - fileOpener: FileOpenerScheme | undefined; -}): React.ReactElement { - switch (item.type) { - case "message": - return ( - - ); - // @ts-expect-error new item types aren't in SDK yet - case "local_shell_call": - case "function_call": - return ; - // @ts-expect-error new item types aren't in SDK yet - case "local_shell_call_output": - case "function_call_output": - return ( - - ); - default: - break; - } - - // @ts-expect-error `reasoning` is not in the responses API yet - if (item.type === "reasoning") { - return ( - - ); - } - - return ; -} - -// TODO: this should be part of `ResponseReasoningItem`. Also it doesn't work. -// --------------------------------------------------------------------------- -// Utility helpers -// --------------------------------------------------------------------------- - -/** - * Guess how long the assistant spent "thinking" based on the combined length - * of the reasoning summary. The calculation itself is fast, but wrapping it in - * `useMemo` in the consuming component ensures it only runs when the - * `summary` array actually changes. - */ -// TODO: use actual thinking time -// -// function guessThinkingTime(summary: Array) { -// const totalTextLength = summary -// .map((t) => t.text.length) -// .reduce((a, b) => a + b, summary.length - 1); -// return Math.max(1, Math.ceil(totalTextLength / 300)); -// } - -export function TerminalChatResponseReasoning({ - message, - fileOpener, -}: { - message: ResponseReasoningItem & { duration_ms?: number }; - fileOpener: FileOpenerScheme | undefined; -}): React.ReactElement | null { - // Only render when there is a reasoning summary - if (!message.summary || message.summary.length === 0) { - return null; - } - return ( - - {message.summary.map((summary, key) => { - const s = summary as { headline?: string; text: string }; - return ( - - {s.headline && {s.headline}} - {s.text} - - ); - })} - - ); -} - -const colorsByRole: Record = { - assistant: "magentaBright", - user: "blueBright", -}; - -function TerminalChatResponseMessage({ - message, - setOverlayMode, - fileOpener, -}: { - message: ResponseInputMessageItem | ResponseOutputMessage; - setOverlayMode?: React.Dispatch>; - fileOpener: FileOpenerScheme | undefined; -}) { - // auto switch to model mode if the system message contains "has been deprecated" - useEffect(() => { - if (message.role === "system") { - const systemMessage = message.content.find( - (c) => c.type === "input_text", - )?.text; - if (systemMessage?.includes("model_not_found")) { - setOverlayMode?.("model"); - } - } - }, [message, setOverlayMode]); - - return ( - - - {message.role === "assistant" ? "codex" : message.role} - - - {message.content - .map( - (c) => - c.type === "output_text" - ? c.text - : c.type === "refusal" - ? c.refusal - : c.type === "input_text" - ? collapseXmlBlocks(c.text) - : c.type === "input_image" - ? "" - : c.type === "input_file" - ? c.filename - : "", // unknown content type - ) - .join(" ")} - - - ); -} - -function TerminalChatResponseToolCall({ - message, -}: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - message: ResponseFunctionToolCallItem | any; -}) { - let workdir: string | undefined; - let cmdReadableText: string | undefined; - if (message.type === "function_call") { - const details = parseToolCall(message); - workdir = details?.workdir; - cmdReadableText = details?.cmdReadableText; - } else if (message.type === "local_shell_call") { - const action = message.action; - workdir = action.working_directory; - cmdReadableText = formatCommandForDisplay(action.command); - } - return ( - - - command - {workdir ? {` (${workdir})`} : ""} - - - $ {cmdReadableText} - - - ); -} - -function TerminalChatResponseToolCallOutput({ - message, - fullStdout, -}: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - message: ResponseFunctionToolCallOutputItem | any; - fullStdout: boolean; -}) { - const { output, metadata } = parseToolCallOutput(message.output); - const { exit_code, duration_seconds } = metadata; - const metadataInfo = useMemo( - () => - [ - typeof exit_code !== "undefined" ? `code: ${exit_code}` : "", - typeof duration_seconds !== "undefined" - ? `duration: ${duration_seconds}s` - : "", - ] - .filter(Boolean) - .join(", "), - [exit_code, duration_seconds], - ); - let displayedContent = output; - if (message.type === "function_call_output" && !fullStdout) { - const lines = displayedContent.split("\n"); - if (lines.length > 4) { - const head = lines.slice(0, 4); - const remaining = lines.length - 4; - displayedContent = [...head, `... (${remaining} more lines)`].join("\n"); - } - } - - // ------------------------------------------------------------------------- - // Colorize diff output: lines starting with '-' in red, '+' in green. - // This makes patches and other diff‑like stdout easier to read. - // We exclude the typical diff file headers ('---', '+++') so they retain - // the default color. This is a best‑effort heuristic and should be safe for - // non‑diff output – only the very first character of a line is inspected. - // ------------------------------------------------------------------------- - const colorizedContent = displayedContent - .split("\n") - .map((line) => { - if (line.startsWith("+") && !line.startsWith("++")) { - return chalk.green(line); - } - if (line.startsWith("-") && !line.startsWith("--")) { - return chalk.red(line); - } - return line; - }) - .join("\n"); - return ( - - - command.stdout{" "} - {metadataInfo ? `(${metadataInfo})` : ""} - - {colorizedContent} - - ); -} - -export function TerminalChatResponseGenericMessage({ - message, -}: { - message: ResponseItem; -}): React.ReactElement { - return {JSON.stringify(message, null, 2)}; -} - -export type MarkdownProps = TerminalRendererOptions & { - children: string; - fileOpener: FileOpenerScheme | undefined; - /** Base path for resolving relative file citation paths. */ - cwd?: string; -}; - -export function Markdown({ - children, - fileOpener, - cwd, - ...options -}: MarkdownProps): React.ReactElement { - const size = useTerminalSize(); - - const rendered = React.useMemo(() => { - const linkifiedMarkdown = rewriteFileCitations(children, fileOpener, cwd); - - // Configure marked for this specific render - setOptions({ - // @ts-expect-error missing parser, space props - renderer: new TerminalRenderer({ ...options, width: size.columns }), - }); - const parsed = parse(linkifiedMarkdown, { async: false }).trim(); - - // Remove the truncation logic - return parsed; - // eslint-disable-next-line react-hooks/exhaustive-deps -- options is an object of primitives - }, [ - children, - size.columns, - size.rows, - fileOpener, - supportsHyperlinks.stdout, - chalk.level, - ]); - - return {rendered}; -} - -/** Regex to match citations for source files (hence the `F:` prefix). */ -const citationRegex = new RegExp( - [ - // Opening marker - "【", - - // Capture group 1: file ID or name (anything except '†') - "F:([^†]+)", - - // Field separator - "†", - - // Capture group 2: start line (digits) - "L(\\d+)", - - // Non-capturing group for optional end line - "(?:", - - // Capture group 3: end line (digits or '?') - "-L(\\d+|\\?)", - - // End of optional group (may not be present) - ")?", - - // Closing marker - "】", - ].join(""), - "g", // Global flag -); - -function rewriteFileCitations( - markdown: string, - fileOpener: FileOpenerScheme | undefined, - cwd: string = process.cwd(), -): string { - citationRegex.lastIndex = 0; - return markdown.replace(citationRegex, (_match, file, start, _end) => { - const absPath = path.resolve(cwd, file); - if (!fileOpener) { - return `[${file}](${absPath})`; - } - const uri = `${fileOpener}://file${absPath}:${start}`; - const label = `${file}:${start}`; - // In practice, sometimes multiple citations for the same file, but with a - // different line number, are shown sequentially, so we: - // - include the line number in the label to disambiguate them - // - add a space after the link to make it easier to read - return `[${label}](${uri}) `; - }); -} diff --git a/codex-cli/src/components/chat/terminal-chat-tool-call-command.tsx b/codex-cli/src/components/chat/terminal-chat-tool-call-command.tsx deleted file mode 100644 index 614ebf382f..0000000000 --- a/codex-cli/src/components/chat/terminal-chat-tool-call-command.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { parseApplyPatch } from "../../parse-apply-patch"; -import { shortenPath } from "../../utils/short-path"; -import chalk from "chalk"; -import { Text } from "ink"; -import React from "react"; - -export function TerminalChatToolCallCommand({ - commandForDisplay, - explanation, -}: { - commandForDisplay: string; - explanation?: string; -}): React.ReactElement { - // ------------------------------------------------------------------------- - // Colorize diff output inside the command preview: we detect individual - // lines that begin with '+' or '-' (excluding the typical diff headers like - // '+++', '---', '++', '--') and apply green/red coloring. This mirrors - // how Git shows diffs and makes the patch easier to review. - // ------------------------------------------------------------------------- - - const colorizedCommand = commandForDisplay - .split("\n") - .map((line) => { - if (line.startsWith("+") && !line.startsWith("++")) { - return chalk.green(line); - } - if (line.startsWith("-") && !line.startsWith("--")) { - return chalk.red(line); - } - return line; - }) - .join("\n"); - - return ( - <> - - Shell Command - - - $ {colorizedCommand} - - {explanation && ( - <> - - Explanation - - {explanation.split("\n").map((line, i) => { - // Apply different styling to headings (numbered items) - if (line.match(/^\d+\.\s+/)) { - return ( - - {line} - - ); - } else if (line.match(/^\s*\*\s+/)) { - // Style bullet points - return ( - - {line} - - ); - } else if (line.match(/^(WARNING|CAUTION|NOTE):/i)) { - // Style warnings - return ( - - {line} - - ); - } else { - return {line}; - } - })} - - )} - - ); -} - -export function TerminalChatToolCallApplyPatch({ - commandForDisplay, - patch, -}: { - commandForDisplay: string; - patch: string; -}): React.ReactElement { - const ops = React.useMemo(() => parseApplyPatch(patch), [patch]); - const firstOp = ops?.[0]; - - const title = React.useMemo(() => { - if (!firstOp) { - return ""; - } - return capitalize(firstOp.type); - }, [firstOp]); - - const filePath = React.useMemo(() => { - if (!firstOp) { - return ""; - } - return shortenPath(firstOp.path || "."); - }, [firstOp]); - - if (ops == null) { - return ( - <> - - Invalid Patch - - - The provided patch command is invalid. - - {commandForDisplay} - - ); - } - - if (!firstOp) { - return ( - <> - - Empty Patch - - - No operations found in the patch command. - - {commandForDisplay} - - ); - } - - return ( - <> - - {title} {filePath} - - - $ {commandForDisplay} - - - ); -} - -const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); diff --git a/codex-cli/src/components/chat/terminal-chat.tsx b/codex-cli/src/components/chat/terminal-chat.tsx deleted file mode 100644 index d41a94990f..0000000000 --- a/codex-cli/src/components/chat/terminal-chat.tsx +++ /dev/null @@ -1,766 +0,0 @@ -import type { AppRollout } from "../../app.js"; -import type { ApplyPatchCommand, ApprovalPolicy } from "../../approvals.js"; -import type { CommandConfirmation } from "../../utils/agent/agent-loop.js"; -import type { AppConfig } from "../../utils/config.js"; -import type { ColorName } from "chalk"; -import type { ResponseItem } from "openai/resources/responses/responses.mjs"; - -import TerminalChatInput from "./terminal-chat-input.js"; -import TerminalChatPastRollout from "./terminal-chat-past-rollout.js"; -import { TerminalChatToolCallCommand } from "./terminal-chat-tool-call-command.js"; -import TerminalMessageHistory from "./terminal-message-history.js"; -import { formatCommandForDisplay } from "../../format-command.js"; -import { useConfirmation } from "../../hooks/use-confirmation.js"; -import { useTerminalSize } from "../../hooks/use-terminal-size.js"; -import { AgentLoop } from "../../utils/agent/agent-loop.js"; -import { ReviewDecision } from "../../utils/agent/review.js"; -import { generateCompactSummary } from "../../utils/compact-summary.js"; -import { saveConfig } from "../../utils/config.js"; -import { extractAppliedPatches as _extractAppliedPatches } from "../../utils/extract-applied-patches.js"; -import { getGitDiff } from "../../utils/get-diff.js"; -import { createInputItem } from "../../utils/input-utils.js"; -import { log } from "../../utils/logger/log.js"; -import { - getAvailableModels, - calculateContextPercentRemaining, - uniqueById, -} from "../../utils/model-utils.js"; -import { createOpenAIClient } from "../../utils/openai-client.js"; -import { shortCwd } from "../../utils/short-path.js"; -import { saveRollout } from "../../utils/storage/save-rollout.js"; -import { CLI_VERSION } from "../../version.js"; -import ApprovalModeOverlay from "../approval-mode-overlay.js"; -import DiffOverlay from "../diff-overlay.js"; -import HelpOverlay from "../help-overlay.js"; -import HistoryOverlay from "../history-overlay.js"; -import ModelOverlay from "../model-overlay.js"; -import SessionsOverlay from "../sessions-overlay.js"; -import chalk from "chalk"; -import fs from "fs/promises"; -import { Box, Text } from "ink"; -import { spawn } from "node:child_process"; -import React, { useEffect, useMemo, useRef, useState } from "react"; -import { inspect } from "util"; - -export type OverlayModeType = - | "none" - | "history" - | "sessions" - | "model" - | "approval" - | "help" - | "diff"; - -type Props = { - config: AppConfig; - prompt?: string; - imagePaths?: Array; - approvalPolicy: ApprovalPolicy; - additionalWritableRoots: ReadonlyArray; - fullStdout: boolean; -}; - -const colorsByPolicy: Record = { - "suggest": undefined, - "auto-edit": "greenBright", - "full-auto": "green", -}; - -/** - * Generates an explanation for a shell command using the OpenAI API. - * - * @param command The command to explain - * @param model The model to use for generating the explanation - * @param flexMode Whether to use the flex-mode service tier - * @param config The configuration object - * @returns A human-readable explanation of what the command does - */ -async function generateCommandExplanation( - command: Array, - model: string, - flexMode: boolean, - config: AppConfig, -): Promise { - try { - // Create a temporary OpenAI client - const oai = createOpenAIClient(config); - - // Format the command for display - const commandForDisplay = formatCommandForDisplay(command); - - // Create a prompt that asks for an explanation with a more detailed system prompt - const response = await oai.chat.completions.create({ - model, - ...(flexMode ? { service_tier: "flex" } : {}), - messages: [ - { - role: "system", - content: - "You are an expert in shell commands and terminal operations. Your task is to provide detailed, accurate explanations of shell commands that users are considering executing. Break down each part of the command, explain what it does, identify any potential risks or side effects, and explain why someone might want to run it. Be specific about what files or systems will be affected. If the command could potentially be harmful, make sure to clearly highlight those risks.", - }, - { - role: "user", - content: `Please explain this shell command in detail: \`${commandForDisplay}\`\n\nProvide a structured explanation that includes:\n1. A brief overview of what the command does\n2. A breakdown of each part of the command (flags, arguments, etc.)\n3. What files, directories, or systems will be affected\n4. Any potential risks or side effects\n5. Why someone might want to run this command\n\nBe specific and technical - this explanation will help the user decide whether to approve or reject the command.`, - }, - ], - }); - - // Extract the explanation from the response - const explanation = - response.choices[0]?.message.content || "Unable to generate explanation."; - return explanation; - } catch (error) { - log(`Error generating command explanation: ${error}`); - - let errorMessage = "Unable to generate explanation due to an error."; - if (error instanceof Error) { - errorMessage = `Unable to generate explanation: ${error.message}`; - - // If it's an API error, check for more specific information - if ("status" in error && typeof error.status === "number") { - // Handle API-specific errors - if (error.status === 401) { - errorMessage = - "Unable to generate explanation: API key is invalid or expired."; - } else if (error.status === 429) { - errorMessage = - "Unable to generate explanation: Rate limit exceeded. Please try again later."; - } else if (error.status >= 500) { - errorMessage = - "Unable to generate explanation: OpenAI service is currently unavailable. Please try again later."; - } - } - } - - return errorMessage; - } -} - -export default function TerminalChat({ - config, - prompt: _initialPrompt, - imagePaths: _initialImagePaths, - approvalPolicy: initialApprovalPolicy, - additionalWritableRoots, - fullStdout, -}: Props): React.ReactElement { - const notify = Boolean(config.notify); - const [model, setModel] = useState(config.model); - const [provider, setProvider] = useState(config.provider || "openai"); - const [lastResponseId, setLastResponseId] = useState(null); - const [items, setItems] = useState>([]); - const [loading, setLoading] = useState(false); - const [approvalPolicy, setApprovalPolicy] = useState( - initialApprovalPolicy, - ); - const [thinkingSeconds, setThinkingSeconds] = useState(0); - - const handleCompact = async () => { - setLoading(true); - try { - const summary = await generateCompactSummary( - items, - model, - Boolean(config.flexMode), - config, - ); - setItems([ - { - id: `compact-${Date.now()}`, - type: "message", - role: "assistant", - content: [{ type: "output_text", text: summary }], - } as ResponseItem, - ]); - } catch (err) { - setItems((prev) => [ - ...prev, - { - id: `compact-error-${Date.now()}`, - type: "message", - role: "system", - content: [ - { type: "input_text", text: `Failed to compact context: ${err}` }, - ], - } as ResponseItem, - ]); - } finally { - setLoading(false); - } - }; - - const { - requestConfirmation, - confirmationPrompt, - explanation, - submitConfirmation, - } = useConfirmation(); - const [overlayMode, setOverlayMode] = useState("none"); - const [viewRollout, setViewRollout] = useState(null); - - // Store the diff text when opening the diff overlay so the view isn’t - // recomputed on every re‑render while it is open. - // diffText is passed down to the DiffOverlay component. The setter is - // currently unused but retained for potential future updates. Prefix with - // an underscore so eslint ignores the unused variable. - const [diffText, _setDiffText] = useState(""); - - const [initialPrompt, setInitialPrompt] = useState(_initialPrompt); - const [initialImagePaths, setInitialImagePaths] = - useState(_initialImagePaths); - - const PWD = React.useMemo(() => shortCwd(), []); - - // Keep a single AgentLoop instance alive across renders; - // recreate only when model/instructions/approvalPolicy change. - const agentRef = React.useRef(); - const [, forceUpdate] = React.useReducer((c) => c + 1, 0); // trigger re‑render - - // ──────────────────────────────────────────────────────────────── - // DEBUG: log every render w/ key bits of state - // ──────────────────────────────────────────────────────────────── - log( - `render - agent? ${Boolean(agentRef.current)} loading=${loading} items=${ - items.length - }`, - ); - - useEffect(() => { - // Skip recreating the agent if awaiting a decision on a pending confirmation. - if (confirmationPrompt != null) { - log("skip AgentLoop recreation due to pending confirmationPrompt"); - return; - } - - log("creating NEW AgentLoop"); - log( - `model=${model} provider=${provider} instructions=${Boolean( - config.instructions, - )} approvalPolicy=${approvalPolicy}`, - ); - - // Tear down any existing loop before creating a new one. - agentRef.current?.terminate(); - - const sessionId = crypto.randomUUID(); - agentRef.current = new AgentLoop({ - model, - provider, - config, - instructions: config.instructions, - approvalPolicy, - disableResponseStorage: config.disableResponseStorage, - additionalWritableRoots, - onLastResponseId: setLastResponseId, - onItem: (item) => { - log(`onItem: ${JSON.stringify(item)}`); - setItems((prev) => { - const updated = uniqueById([...prev, item as ResponseItem]); - saveRollout(sessionId, updated); - return updated; - }); - }, - onLoading: setLoading, - getCommandConfirmation: async ( - command: Array, - applyPatch: ApplyPatchCommand | undefined, - ): Promise => { - log(`getCommandConfirmation: ${command}`); - const commandForDisplay = formatCommandForDisplay(command); - - // First request for confirmation - let { decision: review, customDenyMessage } = await requestConfirmation( - , - ); - - // If the user wants an explanation, generate one and ask again. - if (review === ReviewDecision.EXPLAIN) { - log(`Generating explanation for command: ${commandForDisplay}`); - const explanation = await generateCommandExplanation( - command, - model, - Boolean(config.flexMode), - config, - ); - log(`Generated explanation: ${explanation}`); - - // Ask for confirmation again, but with the explanation. - const confirmResult = await requestConfirmation( - , - ); - - // Update the decision based on the second confirmation. - review = confirmResult.decision; - customDenyMessage = confirmResult.customDenyMessage; - - // Return the final decision with the explanation. - return { review, customDenyMessage, applyPatch, explanation }; - } - - return { review, customDenyMessage, applyPatch }; - }, - }); - - // Force a render so JSX below can "see" the freshly created agent. - forceUpdate(); - - log(`AgentLoop created: ${inspect(agentRef.current, { depth: 1 })}`); - - return () => { - log("terminating AgentLoop"); - agentRef.current?.terminate(); - agentRef.current = undefined; - forceUpdate(); // re‑render after teardown too - }; - // We intentionally omit 'approvalPolicy' and 'confirmationPrompt' from the deps - // so switching modes or showing confirmation dialogs doesn’t tear down the loop. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [model, provider, config, requestConfirmation, additionalWritableRoots]); - - // Whenever loading starts/stops, reset or start a timer — but pause the - // timer while a confirmation overlay is displayed so we don't trigger a - // re‑render every second during apply_patch reviews. - useEffect(() => { - let handle: ReturnType | null = null; - // Only tick the "thinking…" timer when the agent is actually processing - // a request *and* the user is not being asked to review a command. - if (loading && confirmationPrompt == null) { - setThinkingSeconds(0); - handle = setInterval(() => { - setThinkingSeconds((s) => s + 1); - }, 1000); - } else { - if (handle) { - clearInterval(handle); - } - setThinkingSeconds(0); - } - return () => { - if (handle) { - clearInterval(handle); - } - }; - }, [loading, confirmationPrompt]); - - // Notify desktop with a preview when an assistant response arrives. - const prevLoadingRef = useRef(false); - useEffect(() => { - // Only notify when notifications are enabled. - if (!notify) { - prevLoadingRef.current = loading; - return; - } - - if ( - prevLoadingRef.current && - !loading && - confirmationPrompt == null && - items.length > 0 - ) { - if (process.platform === "darwin") { - // find the last assistant message - const assistantMessages = items.filter( - (i) => i.type === "message" && i.role === "assistant", - ); - const last = assistantMessages[assistantMessages.length - 1]; - if (last) { - const text = last.content - .map((c) => { - if (c.type === "output_text") { - return c.text; - } - return ""; - }) - .join("") - .trim(); - const preview = text.replace(/\n/g, " ").slice(0, 100); - const safePreview = preview.replace(/"/g, '\\"'); - const title = "Codex CLI"; - const cwd = PWD; - spawn("osascript", [ - "-e", - `display notification "${safePreview}" with title "${title}" subtitle "${cwd}" sound name "Ping"`, - ]); - } - } - } - prevLoadingRef.current = loading; - }, [notify, loading, confirmationPrompt, items, PWD]); - - // Let's also track whenever the ref becomes available. - const agent = agentRef.current; - useEffect(() => { - log(`agentRef.current is now ${Boolean(agent)}`); - }, [agent]); - - // --------------------------------------------------------------------- - // Dynamic layout constraints – keep total rendered rows <= terminal rows - // --------------------------------------------------------------------- - - const { rows: terminalRows } = useTerminalSize(); - - useEffect(() => { - const processInitialInputItems = async () => { - if ( - (!initialPrompt || initialPrompt.trim() === "") && - (!initialImagePaths || initialImagePaths.length === 0) - ) { - return; - } - const inputItems = [ - await createInputItem(initialPrompt || "", initialImagePaths || []), - ]; - // Clear them to prevent subsequent runs. - setInitialPrompt(""); - setInitialImagePaths([]); - agent?.run(inputItems); - }; - processInitialInputItems(); - }, [agent, initialPrompt, initialImagePaths]); - - // ──────────────────────────────────────────────────────────────── - // In-app warning if CLI --model isn't in fetched list - // ──────────────────────────────────────────────────────────────── - useEffect(() => { - (async () => { - const available = await getAvailableModels(provider); - if (model && available.length > 0 && !available.includes(model)) { - setItems((prev) => [ - ...prev, - { - id: `unknown-model-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: `Warning: model "${model}" is not in the list of available models for provider "${provider}".`, - }, - ], - }, - ]); - } - })(); - // run once on mount - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // Just render every item in order, no grouping/collapse. - const lastMessageBatch = items.map((item) => ({ item })); - const groupCounts: Record = {}; - const userMsgCount = items.filter( - (i) => i.type === "message" && i.role === "user", - ).length; - - const contextLeftPercent = useMemo( - () => calculateContextPercentRemaining(items, model), - [items, model], - ); - - if (viewRollout) { - return ( - - ); - } - - return ( - - - {agent ? ( - - ) : ( - - Initializing agent… - - )} - {overlayMode === "none" && agent && ( - - submitConfirmation({ - decision, - customDenyMessage, - }) - } - contextLeftPercent={contextLeftPercent} - openOverlay={() => setOverlayMode("history")} - openModelOverlay={() => setOverlayMode("model")} - openApprovalOverlay={() => setOverlayMode("approval")} - openHelpOverlay={() => setOverlayMode("help")} - openSessionsOverlay={() => setOverlayMode("sessions")} - openDiffOverlay={() => { - const { isGitRepo, diff } = getGitDiff(); - let text: string; - if (isGitRepo) { - text = diff; - } else { - text = "`/diff` — _not inside a git repository_"; - } - setItems((prev) => [ - ...prev, - { - id: `diff-${Date.now()}`, - type: "message", - role: "system", - content: [{ type: "input_text", text }], - }, - ]); - // Ensure no overlay is shown. - setOverlayMode("none"); - }} - onCompact={handleCompact} - active={overlayMode === "none"} - interruptAgent={() => { - if (!agent) { - return; - } - log( - "TerminalChat: interruptAgent invoked – calling agent.cancel()", - ); - agent.cancel(); - setLoading(false); - - // Add a system message to indicate the interruption - setItems((prev) => [ - ...prev, - { - id: `interrupt-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: "⏹️ Execution interrupted by user. You can continue typing.", - }, - ], - }, - ]); - }} - submitInput={(inputs) => { - agent.run(inputs, lastResponseId || ""); - return {}; - }} - items={items} - thinkingSeconds={thinkingSeconds} - /> - )} - {overlayMode === "history" && ( - setOverlayMode("none")} /> - )} - {overlayMode === "sessions" && ( - { - try { - const txt = await fs.readFile(p, "utf-8"); - const data = JSON.parse(txt) as AppRollout; - setViewRollout(data); - setOverlayMode("none"); - } catch { - setOverlayMode("none"); - } - }} - onResume={(p) => { - setOverlayMode("none"); - setInitialPrompt(`Resume this session: ${p}`); - }} - onExit={() => setOverlayMode("none")} - /> - )} - {overlayMode === "model" && ( - { - log( - "TerminalChat: interruptAgent invoked – calling agent.cancel()", - ); - if (!agent) { - log("TerminalChat: agent is not ready yet"); - } - agent?.cancel(); - setLoading(false); - - if (!allModels?.includes(newModel)) { - // eslint-disable-next-line no-console - console.error( - chalk.bold.red( - `Model "${chalk.yellow( - newModel, - )}" is not available for provider "${chalk.yellow( - provider, - )}".`, - ), - ); - return; - } - - setModel(newModel); - setLastResponseId((prev) => - prev && newModel !== model ? null : prev, - ); - - // Save model to config - saveConfig({ - ...config, - model: newModel, - provider: provider, - }); - - setItems((prev) => [ - ...prev, - { - id: `switch-model-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: `Switched model to ${newModel}`, - }, - ], - }, - ]); - - setOverlayMode("none"); - }} - onSelectProvider={(newProvider) => { - log( - "TerminalChat: interruptAgent invoked – calling agent.cancel()", - ); - if (!agent) { - log("TerminalChat: agent is not ready yet"); - } - agent?.cancel(); - setLoading(false); - - // Select default model for the new provider. - const defaultModel = model; - - // Save provider to config. - const updatedConfig = { - ...config, - provider: newProvider, - model: defaultModel, - }; - saveConfig(updatedConfig); - - setProvider(newProvider); - setModel(defaultModel); - setLastResponseId((prev) => - prev && newProvider !== provider ? null : prev, - ); - - setItems((prev) => [ - ...prev, - { - id: `switch-provider-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: `Switched provider to ${newProvider} with model ${defaultModel}`, - }, - ], - }, - ]); - - // Don't close the overlay so user can select a model for the new provider - // setOverlayMode("none"); - }} - onExit={() => setOverlayMode("none")} - /> - )} - - {overlayMode === "approval" && ( - { - // Update approval policy without cancelling an in-progress session. - if (newMode === approvalPolicy) { - return; - } - - setApprovalPolicy(newMode as ApprovalPolicy); - if (agentRef.current) { - ( - agentRef.current as unknown as { - approvalPolicy: ApprovalPolicy; - } - ).approvalPolicy = newMode as ApprovalPolicy; - } - setItems((prev) => [ - ...prev, - { - id: `switch-approval-${Date.now()}`, - type: "message", - role: "system", - content: [ - { - type: "input_text", - text: `Switched approval mode to ${newMode}`, - }, - ], - }, - ]); - - setOverlayMode("none"); - }} - onExit={() => setOverlayMode("none")} - /> - )} - - {overlayMode === "help" && ( - setOverlayMode("none")} /> - )} - - {overlayMode === "diff" && ( - setOverlayMode("none")} - /> - )} - - - ); -} diff --git a/codex-cli/src/components/chat/terminal-header.tsx b/codex-cli/src/components/chat/terminal-header.tsx deleted file mode 100644 index 9ba16e6fde..0000000000 --- a/codex-cli/src/components/chat/terminal-header.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import type { AgentLoop } from "../../utils/agent/agent-loop.js"; - -import { Box, Text } from "ink"; -import path from "node:path"; -import React from "react"; - -export interface TerminalHeaderProps { - terminalRows: number; - version: string; - PWD: string; - model: string; - provider?: string; - approvalPolicy: string; - colorsByPolicy: Record; - agent?: AgentLoop; - initialImagePaths?: Array; - flexModeEnabled?: boolean; -} - -const TerminalHeader: React.FC = ({ - terminalRows, - version, - PWD, - model, - provider = "openai", - approvalPolicy, - colorsByPolicy, - agent, - initialImagePaths, - flexModeEnabled = false, -}) => { - return ( - <> - {terminalRows < 10 ? ( - // Compact header for small terminal windows - - ● Codex v{version} - {PWD} - {model} ({provider}) -{" "} - {approvalPolicy} - {flexModeEnabled ? " - flex-mode" : ""} - - ) : ( - <> - - - ● OpenAI Codex{" "} - - (research preview) v{version} - - - - - - localhost session:{" "} - - {agent?.sessionId ?? ""} - - - - workdir: {PWD} - - - model: {model} - - - provider:{" "} - {provider} - - - approval:{" "} - - {approvalPolicy} - - - {flexModeEnabled && ( - - flex-mode:{" "} - enabled - - )} - {initialImagePaths?.map((img, idx) => ( - - image:{" "} - {path.basename(img)} - - ))} - - - )} - - ); -}; - -export default TerminalHeader; diff --git a/codex-cli/src/components/chat/terminal-message-history.tsx b/codex-cli/src/components/chat/terminal-message-history.tsx deleted file mode 100644 index 5036f0813d..0000000000 --- a/codex-cli/src/components/chat/terminal-message-history.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import type { OverlayModeType } from "./terminal-chat.js"; -import type { TerminalHeaderProps } from "./terminal-header.js"; -import type { GroupedResponseItem } from "./use-message-grouping.js"; -import type { ResponseItem } from "openai/resources/responses/responses.mjs"; -import type { FileOpenerScheme } from "src/utils/config.js"; - -import TerminalChatResponseItem from "./terminal-chat-response-item.js"; -import TerminalHeader from "./terminal-header.js"; -import { Box, Static } from "ink"; -import React, { useMemo } from "react"; - -// A batch entry can either be a standalone response item or a grouped set of -// items (e.g. auto‑approved tool‑call batches) that should be rendered -// together. -type BatchEntry = { item?: ResponseItem; group?: GroupedResponseItem }; -type TerminalMessageHistoryProps = { - batch: Array; - groupCounts: Record; - items: Array; - userMsgCount: number; - confirmationPrompt: React.ReactNode; - loading: boolean; - thinkingSeconds: number; - headerProps: TerminalHeaderProps; - fullStdout: boolean; - setOverlayMode: React.Dispatch>; - fileOpener: FileOpenerScheme | undefined; -}; - -const TerminalMessageHistory: React.FC = ({ - batch, - headerProps, - // `loading` and `thinkingSeconds` handled by input component now. - loading: _loading, - thinkingSeconds: _thinkingSeconds, - fullStdout, - setOverlayMode, - fileOpener, -}) => { - // Flatten batch entries to response items. - const messages = useMemo(() => batch.map(({ item }) => item!), [batch]); - - return ( - - {/* The dedicated thinking indicator in the input area now displays the - elapsed time, so we no longer render a separate counter here. */} - - {(item, index) => { - if (item === "header") { - return ; - } - - // After the guard above, item is a ResponseItem - const message = item as ResponseItem; - // Suppress empty reasoning updates (i.e. items with an empty summary). - const msg = message as unknown as { summary?: Array }; - if (msg.summary?.length === 0) { - return null; - } - return ( - - - - ); - }} - - - ); -}; - -export default React.memo(TerminalMessageHistory); diff --git a/codex-cli/src/components/chat/use-message-grouping.ts b/codex-cli/src/components/chat/use-message-grouping.ts deleted file mode 100644 index 1e526821d0..0000000000 --- a/codex-cli/src/components/chat/use-message-grouping.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { ResponseItem } from "openai/resources/responses/responses.mjs"; - -/** - * Represents a grouped sequence of response items (e.g., function call batches). - */ -export type GroupedResponseItem = { - label: string; - items: Array; -}; diff --git a/codex-cli/src/components/diff-overlay.tsx b/codex-cli/src/components/diff-overlay.tsx deleted file mode 100644 index 8de85b87d5..0000000000 --- a/codex-cli/src/components/diff-overlay.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { Box, Text, useInput } from "ink"; -import React, { useState } from "react"; - -/** - * Simple scrollable view for displaying a diff. - * The component is intentionally lightweight and mirrors the UX of - * HistoryOverlay: Up/Down or j/k to scroll, PgUp/PgDn for paging and Esc to - * close. The caller is responsible for computing the diff text. - */ -export default function DiffOverlay({ - diffText, - onExit, -}: { - diffText: string; - onExit: () => void; -}): JSX.Element { - const lines = diffText.length > 0 ? diffText.split("\n") : ["(no changes)"]; - - const [cursor, setCursor] = useState(0); - - // Determine how many rows we can display – similar to HistoryOverlay. - const rows = process.stdout.rows || 24; - const headerRows = 2; - const footerRows = 1; - const maxVisible = Math.max(4, rows - headerRows - footerRows); - - useInput((input, key) => { - if (key.escape || input === "q") { - onExit(); - return; - } - - if (key.downArrow || input === "j") { - setCursor((c) => Math.min(lines.length - 1, c + 1)); - } else if (key.upArrow || input === "k") { - setCursor((c) => Math.max(0, c - 1)); - } else if (key.pageDown) { - setCursor((c) => Math.min(lines.length - 1, c + maxVisible)); - } else if (key.pageUp) { - setCursor((c) => Math.max(0, c - maxVisible)); - } else if (input === "g") { - setCursor(0); - } else if (input === "G") { - setCursor(lines.length - 1); - } - }); - - const firstVisible = Math.min( - Math.max(0, cursor - Math.floor(maxVisible / 2)), - Math.max(0, lines.length - maxVisible), - ); - const visible = lines.slice(firstVisible, firstVisible + maxVisible); - - // Very small helper to colorize diff lines in a basic way. - function renderLine(line: string, idx: number): JSX.Element { - let color: "green" | "red" | "cyan" | undefined = undefined; - if (line.startsWith("+")) { - color = "green"; - } else if (line.startsWith("-")) { - color = "red"; - } else if (line.startsWith("@@") || line.startsWith("diff --git")) { - color = "cyan"; - } - return ( - - {line === "" ? " " : line} - - ); - } - - return ( - - - Working tree diff ({lines.length} lines) - - - - {visible.map((line, idx) => { - return renderLine(line, firstVisible + idx); - })} - - - - esc Close ↑↓ Scroll PgUp/PgDn g/G First/Last - - - ); -} diff --git a/codex-cli/src/components/help-overlay.tsx b/codex-cli/src/components/help-overlay.tsx deleted file mode 100644 index 1c24ad9c72..0000000000 --- a/codex-cli/src/components/help-overlay.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { Box, Text, useInput } from "ink"; -import React from "react"; - -/** - * An overlay that lists the available slash‑commands and their description. - * The overlay is purely informational and can be dismissed with the Escape - * key. Keeping the implementation extremely small avoids adding any new - * dependencies or complex state handling. - */ -export default function HelpOverlay({ - onExit, -}: { - onExit: () => void; -}): JSX.Element { - useInput((input, key) => { - if (key.escape || input === "q") { - onExit(); - } - }); - - return ( - - - Available commands - - - - - Slash‑commands - - - /help – show this help overlay - - - /model – switch the LLM model in‑session - - - /approval – switch auto‑approval mode - - - /history – show command & file history - for this session - - - /clear – clear screen & context - - - /clearhistory – clear command history - - - /bug – generate a prefilled GitHub issue URL - with session log - - - /diff – view working tree git diff - - - /compact – condense context into a summary - - - - - Keyboard shortcuts - - - - Enter – send message - - - Ctrl+J – insert newline - - {/* Re-enable once we re-enable new input */} - {/* - - Ctrl+X/Ctrl+E -  – open external editor ($EDITOR) - - */} - - Up/Down – scroll prompt history - - - - Esc(✕2) - {" "} - – interrupt current action - - - Ctrl+C – quit Codex - - - - - Esc or q to close - - - ); -} diff --git a/codex-cli/src/components/history-overlay.tsx b/codex-cli/src/components/history-overlay.tsx deleted file mode 100644 index f6ea8464e1..0000000000 --- a/codex-cli/src/components/history-overlay.tsx +++ /dev/null @@ -1,255 +0,0 @@ -import type { ResponseItem } from "openai/resources/responses/responses.mjs"; - -import { Box, Text, useInput } from "ink"; -import React, { useMemo, useState } from "react"; - -type Props = { - items: Array; - onExit: () => void; -}; - -type Mode = "commands" | "files"; - -export default function HistoryOverlay({ items, onExit }: Props): JSX.Element { - const [mode, setMode] = useState("commands"); - const [cursor, setCursor] = useState(0); - - const { commands, files } = useMemo( - () => formatHistoryForDisplay(items), - [items], - ); - - const list = mode === "commands" ? commands : files; - - useInput((input, key) => { - if (key.escape) { - onExit(); - return; - } - - if (input === "c") { - setMode("commands"); - setCursor(0); - return; - } - if (input === "f") { - setMode("files"); - setCursor(0); - return; - } - - if (key.downArrow || input === "j") { - setCursor((c) => Math.min(list.length - 1, c + 1)); - } else if (key.upArrow || input === "k") { - setCursor((c) => Math.max(0, c - 1)); - } else if (key.pageDown) { - setCursor((c) => Math.min(list.length - 1, c + 10)); - } else if (key.pageUp) { - setCursor((c) => Math.max(0, c - 10)); - } else if (input === "g") { - setCursor(0); - } else if (input === "G") { - setCursor(list.length - 1); - } - }); - - const rows = process.stdout.rows || 24; - const headerRows = 2; - const footerRows = 1; - const maxVisible = Math.max(4, rows - headerRows - footerRows); - - const firstVisible = Math.min( - Math.max(0, cursor - Math.floor(maxVisible / 2)), - Math.max(0, list.length - maxVisible), - ); - const visible = list.slice(firstVisible, firstVisible + maxVisible); - - return ( - - - - {mode === "commands" ? "Commands run" : "Files touched"} ( - {list.length}) - - - - {visible.map((txt, idx) => { - const absIdx = firstVisible + idx; - const selected = absIdx === cursor; - return ( - - {selected ? "› " : " "} - {txt} - - ); - })} - - - - esc Close ↑↓ Scroll PgUp/PgDn g/G First/Last c Commands f Files - - - - ); -} - -function formatHistoryForDisplay(items: Array): { - commands: Array; - files: Array; -} { - const commands: Array = []; - const filesSet = new Set(); - - for (const item of items) { - const userPrompt = processUserMessage(item); - if (userPrompt) { - commands.push(userPrompt); - continue; - } - - // ------------------------------------------------------------------ - // We are interested in tool calls which – for the OpenAI client – are - // represented as `function_call` response items. Skip everything else. - if (item.type !== "function_call") { - continue; - } - - const { name: toolName, arguments: argsString } = item as unknown as { - name: unknown; - arguments: unknown; - }; - - if (typeof argsString !== "string") { - // Malformed – still record the tool name to give users maximal context. - if (typeof toolName === "string" && toolName.length > 0) { - commands.push(toolName); - } - continue; - } - - // Best‑effort attempt to parse the JSON arguments. We never throw on parse - // failure – the history view must be resilient to bad data. - let argsJson: unknown = undefined; - try { - argsJson = JSON.parse(argsString); - } catch { - argsJson = undefined; - } - - // 1) Shell / exec‑like tool calls expose a `cmd` or `command` property - // that is an array of strings. These are rendered as the joined command - // line for familiarity with traditional shells. - const argsObj = argsJson as Record | undefined; - const cmdArray: Array | undefined = Array.isArray(argsObj?.["cmd"]) - ? (argsObj!["cmd"] as Array) - : Array.isArray(argsObj?.["command"]) - ? (argsObj!["command"] as Array) - : undefined; - - if (cmdArray && cmdArray.length > 0) { - commands.push(processCommandArray(cmdArray, filesSet)); - continue; // We processed this as a command; no need to treat as generic tool call. - } - - // 2) Non‑exec tool calls – we fall back to recording the tool name plus a - // short argument representation to give users an idea of what - // happened. - if (typeof toolName === "string" && toolName.length > 0) { - commands.push(processNonExecTool(toolName, argsJson, filesSet)); - } - } - - return { commands, files: Array.from(filesSet) }; -} - -function processUserMessage(item: ResponseItem): string | null { - if ( - item.type === "message" && - (item as unknown as { role?: string }).role === "user" - ) { - // TODO: We're ignoring images/files here. - const parts = - (item as unknown as { content?: Array }).content ?? []; - const texts: Array = []; - if (Array.isArray(parts)) { - for (const part of parts) { - if (part && typeof part === "object" && "text" in part) { - const t = (part as unknown as { text?: string }).text; - if (typeof t === "string" && t.length > 0) { - texts.push(t); - } - } - } - } - - if (texts.length > 0) { - const fullPrompt = texts.join(" "); - // Truncate very long prompts so the history view stays legible. - return fullPrompt.length > 120 - ? `> ${fullPrompt.slice(0, 117)}…` - : `> ${fullPrompt}`; - } - } - return null; -} - -function processCommandArray( - cmdArray: Array, - filesSet: Set, -): string { - const cmd = cmdArray.join(" "); - - // Heuristic for file paths in command args - for (const part of cmdArray) { - if (!part.startsWith("-") && part.includes("/")) { - filesSet.add(part); - } - } - - // Special‑case apply_patch so we can extract the list of modified files - if (cmdArray[0] === "apply_patch" || cmdArray.includes("apply_patch")) { - const patchTextMaybe = cmdArray.find((s) => s.includes("*** Begin Patch")); - if (typeof patchTextMaybe === "string") { - const lines = patchTextMaybe.split("\n"); - for (const line of lines) { - const m = line.match(/^[-+]{3} [ab]\/(.+)$/); - if (m && m[1]) { - filesSet.add(m[1]); - } - } - } - } - - return cmd; -} - -function processNonExecTool( - toolName: string, - argsJson: unknown, - filesSet: Set, -): string { - let summary = toolName; - - if (argsJson && typeof argsJson === "object") { - // Extract a few common argument keys to make the summary more useful - // without being overly verbose. - const interestingKeys = ["path", "file", "filepath", "filename", "pattern"]; - for (const key of interestingKeys) { - const val = (argsJson as Record)[key]; - if (typeof val === "string") { - summary += ` ${val}`; - if (val.includes("/")) { - filesSet.add(val); - } - break; - } - } - } - - return summary; -} diff --git a/codex-cli/src/components/model-overlay.tsx b/codex-cli/src/components/model-overlay.tsx deleted file mode 100644 index 86a7e5850d..0000000000 --- a/codex-cli/src/components/model-overlay.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import TypeaheadOverlay from "./typeahead-overlay.js"; -import { - getAvailableModels, - RECOMMENDED_MODELS as _RECOMMENDED_MODELS, -} from "../utils/model-utils.js"; -import { Box, Text, useInput } from "ink"; -import React, { useEffect, useState } from "react"; - -/** - * Props for . - * - * When `hasLastResponse` is true the user has already received at least one - * assistant response in the current session which means switching models is no - * longer supported – the overlay should therefore show an error and only allow - * the user to close it. - */ -type Props = { - currentModel: string; - currentProvider?: string; - hasLastResponse: boolean; - providers?: Record; - onSelect: (allModels: Array, model: string) => void; - onSelectProvider?: (provider: string) => void; - onExit: () => void; -}; - -export default function ModelOverlay({ - currentModel, - providers = {}, - currentProvider = "openai", - hasLastResponse, - onSelect, - onSelectProvider, - onExit, -}: Props): JSX.Element { - const [items, setItems] = useState>( - [], - ); - const [providerItems, _setProviderItems] = useState< - Array<{ label: string; value: string }> - >(Object.values(providers).map((p) => ({ label: p.name, value: p.name }))); - const [mode, setMode] = useState<"model" | "provider">("model"); - const [isLoading, setIsLoading] = useState(true); - - // This effect will run when the provider changes to update the model list - useEffect(() => { - setIsLoading(true); - (async () => { - try { - const models = await getAvailableModels(currentProvider); - // Convert the models to the format needed by TypeaheadOverlay - setItems( - models.map((m) => ({ - label: m, - value: m, - })), - ); - } catch (error) { - // Silently handle errors - remove console.error - // console.error("Error loading models:", error); - } finally { - setIsLoading(false); - } - })(); - }, [currentProvider]); - - // --------------------------------------------------------------------------- - // If the conversation already contains a response we cannot change the model - // anymore because the backend requires a consistent model across the entire - // run. In that scenario we replace the regular typeahead picker with a - // simple message instructing the user to start a new chat. The only - // available action is to dismiss the overlay (Esc or Enter). - // --------------------------------------------------------------------------- - - // Register input handling for switching between model and provider selection - useInput((_input, key) => { - if (hasLastResponse && (key.escape || key.return)) { - onExit(); - } else if (!hasLastResponse) { - if (key.tab) { - setMode(mode === "model" ? "provider" : "model"); - } - } - }); - - if (hasLastResponse) { - return ( - - - - Unable to switch model - - - - - You can only pick a model before the assistant sends its first - response. To use a different model please start a new chat. - - - - press esc or enter to close - - - ); - } - - if (mode === "provider") { - return ( - - - Current provider:{" "} - {currentProvider} - - press tab to switch to model selection - - } - initialItems={providerItems} - currentValue={currentProvider} - onSelect={(provider) => { - if (onSelectProvider) { - onSelectProvider(provider); - // Immediately switch to model selection so user can pick a model for the new provider - setMode("model"); - } - }} - onExit={onExit} - /> - ); - } - - return ( - - - Current model: {currentModel} - - - Current provider: {currentProvider} - - {isLoading && Loading models...} - press tab to switch to provider selection - - } - initialItems={items} - currentValue={currentModel} - onSelect={(selectedModel) => - onSelect( - items?.map((m) => m.value), - selectedModel, - ) - } - onExit={onExit} - /> - ); -} diff --git a/codex-cli/src/components/onboarding/onboarding-approval-mode.tsx b/codex-cli/src/components/onboarding/onboarding-approval-mode.tsx deleted file mode 100644 index f095c6c04b..0000000000 --- a/codex-cli/src/components/onboarding/onboarding-approval-mode.tsx +++ /dev/null @@ -1,35 +0,0 @@ -// @ts-expect-error select.js is JavaScript and has no types -import { Select } from "../vendor/ink-select/select"; -import { Box, Text } from "ink"; -import React from "react"; -import { AutoApprovalMode } from "src/utils/auto-approval-mode"; - -// TODO: figure out why `cli-spinners` fails on Node v20.9.0 -// which is why we have to do this in the first place - -export function OnboardingApprovalMode(): React.ReactElement { - return ( - - Choose what you want to have to approve: - [+ cached] - let mut input_line_spans: Vec> = vec![ - " • Input: ".into(), - usage.non_cached_input().to_string().into(), - ]; - if let Some(cached) = usage.cached_input_tokens { - if cached > 0 { - input_line_spans.push(format!(" (+ {cached} cached)").into()); - } - } - lines.push(Line::from(input_line_spans)); - // Output: - lines.push(Line::from(vec![ - " • Output: ".into(), - usage.output_tokens.to_string().into(), - ])); - // Total: - lines.push(Line::from(vec![ - " • Total: ".into(), - usage.blended_total().to_string().into(), - ])); - - lines.push(Line::from("")); - HistoryCell::StatusOutput { - view: TextBlock::new(lines), - } - } - - pub(crate) fn new_prompts_output() -> Self { let lines: Vec> = vec![ - Line::from("/prompts".magenta()), - Line::from(""), - Line::from(" 1. Explain this codebase"), - Line::from(" 2. Summarize recent commits"), - Line::from(" 3. Implement {feature}"), - Line::from(" 4. Find and fix a bug in @filename"), - Line::from(" 5. Write tests for @filename"), - Line::from(" 6. Improve documentation in @filename"), + Line::from(vec![ + Span::raw(">_ ").dim(), + Span::styled( + "You are using OpenAI Codex in", + Style::default().add_modifier(Modifier::BOLD), + ), + Span::raw(format!(" {cwd_str}")).dim(), + ]), + Line::from("".dim()), + Line::from(" To get started, describe a task or try one of these commands:".dim()), + Line::from("".dim()), + Line::from(format!(" /init - {}", SlashCommand::Init.description()).dim()), + Line::from(format!(" /status - {}", SlashCommand::Status.description()).dim()), + Line::from(format!(" /diff - {}", SlashCommand::Diff.description()).dim()), + Line::from(format!(" /prompts - {}", SlashCommand::Prompts.description()).dim()), + Line::from("".dim()), + ]; + PlainHistoryCell { lines } + } else if config.model == model { + PlainHistoryCell { lines: Vec::new() } + } else { + let lines = vec![ + Line::from("model changed:".magenta().bold()), + Line::from(format!("requested: {}", config.model)), + Line::from(format!("used: {model}")), Line::from(""), ]; - HistoryCell::PromptsOutput { - view: TextBlock::new(lines), - } - } - - pub(crate) fn new_error_event(message: String) -> Self { - let lines: Vec> = - vec![vec!["🖐 ".red().bold(), message.into()].into(), "".into()]; - HistoryCell::ErrorEvent { - view: TextBlock::new(lines), - } - } - - /// Render a user‑friendly plan update styled like a checkbox todo list. - pub(crate) fn new_plan_update(update: UpdatePlanArgs) -> Self { - let UpdatePlanArgs { explanation, plan } = update; - - let mut lines: Vec> = Vec::new(); - // Header with progress summary - let total = plan.len(); - let completed = plan - .iter() - .filter(|p| matches!(p.status, StepStatus::Completed)) - .count(); - - let width: usize = 10; - let filled = if total > 0 { - (completed * width + total / 2) / total - } else { - 0 - }; - let empty = width.saturating_sub(filled); - - let mut header: Vec = Vec::new(); - header.push(Span::raw("📋")); - header.push(Span::styled( - " Update plan", - Style::default().add_modifier(Modifier::BOLD).magenta(), - )); - header.push(Span::raw(" [")); - if filled > 0 { - header.push(Span::styled( - "█".repeat(filled), - Style::default().fg(Color::Green), - )); - } - if empty > 0 { - header.push(Span::styled( - "░".repeat(empty), - Style::default().add_modifier(Modifier::DIM), - )); - } - header.push(Span::raw("] ")); - header.push(Span::raw(format!("{completed}/{total}"))); - lines.push(Line::from(header)); - - // Optional explanation/note from the model - if let Some(expl) = explanation.and_then(|s| { - let t = s.trim().to_string(); - if t.is_empty() { None } else { Some(t) } - }) { - lines.push(Line::from("note".dim().italic())); - for l in expl.lines() { - lines.push(Line::from(l.to_string()).dim()); - } - } - - // Steps styled as checkbox items - if plan.is_empty() { - lines.push(Line::from("(no steps provided)".dim().italic())); - } else { - for (idx, PlanItemArg { step, status }) in plan.into_iter().enumerate() { - let (box_span, text_span) = match status { - StepStatus::Completed => ( - Span::styled("✔", Style::default().fg(Color::Green)), - Span::styled( - step, - Style::default().add_modifier(Modifier::CROSSED_OUT | Modifier::DIM), - ), - ), - StepStatus::InProgress => ( - Span::raw("□"), - Span::styled( - step, - Style::default() - .fg(Color::Blue) - .add_modifier(Modifier::BOLD), - ), - ), - StepStatus::Pending => ( - Span::raw("□"), - Span::styled(step, Style::default().add_modifier(Modifier::DIM)), - ), - }; - let prefix = if idx == 0 { - Span::raw(" └ ") - } else { - Span::raw(" ") - }; - lines.push(Line::from(vec![ - prefix, - box_span, - Span::raw(" "), - text_span, - ])); - } - } - - lines.push(Line::from("")); - - HistoryCell::PlanUpdate { - view: TextBlock::new(lines), - } - } - - /// Create a new `PendingPatch` cell that lists the file‑level summary of - /// a proposed patch. The summary lines should already be formatted (e.g. - /// "A path/to/file.rs"). - pub(crate) fn new_patch_event( - event_type: PatchEventType, - changes: HashMap, - ) -> Self { - let title = match &event_type { - PatchEventType::ApprovalRequest => "proposed patch", - PatchEventType::ApplyBegin { - auto_approved: true, - } => "✏️ Applying patch", - PatchEventType::ApplyBegin { - auto_approved: false, - } => { - let lines: Vec> = vec![ - Line::from("✏️ Applying patch".magenta().bold()), - Line::from(""), - ]; - return Self::PendingPatch { - view: TextBlock::new(lines), - }; - } - }; - - let mut lines: Vec> = create_diff_summary(title, &changes, event_type); - - lines.push(Line::from("")); - - HistoryCell::PendingPatch { - view: TextBlock::new(lines), - } - } - - pub(crate) fn new_patch_apply_failure(stderr: String) -> Self { - let mut lines: Vec> = Vec::new(); - - // Failure title - lines.push(Line::from("✘ Failed to apply patch".magenta().bold())); - - if !stderr.trim().is_empty() { - lines.extend(output_lines( - Some(&CommandOutput { - exit_code: 1, - stdout: String::new(), - stderr, - }), - true, - true, - )); - } - - lines.push(Line::from("")); - - HistoryCell::PatchApplyResult { - view: TextBlock::new(lines), - } - } - - pub(crate) fn new_patch_apply_success(stdout: String) -> Self { - let mut lines: Vec> = Vec::new(); - - // Success title - lines.push(Line::from("✓ Applied patch".magenta().bold())); - - if !stdout.trim().is_empty() { - let mut iter = stdout.lines(); - for (i, raw) in iter.by_ref().take(TOOL_CALL_MAX_LINES).enumerate() { - let prefix = if i == 0 { " └ " } else { " " }; - let s = format!("{prefix}{raw}"); - lines.push(ansi_escape_line(&s).dim()); - } - let remaining = iter.count(); - if remaining > 0 { - lines.push(Line::from("")); - lines.push(Line::from(format!("... +{remaining} lines")).dim()); - } - } - - lines.push(Line::from("")); - - HistoryCell::PatchApplyResult { - view: TextBlock::new(lines), - } + PlainHistoryCell { lines } } } -impl WidgetRef for &HistoryCell { - fn render_ref(&self, area: Rect, buf: &mut Buffer) { - Paragraph::new(Text::from(self.plain_lines())) - .wrap(Wrap { trim: false }) - .render(area, buf); +pub(crate) fn new_user_prompt(message: String) -> PlainHistoryCell { + let mut lines: Vec> = Vec::new(); + lines.push(Line::from("user".cyan().bold())); + lines.extend(message.lines().map(|l| Line::from(l.to_string()))); + lines.push(Line::from("")); + + PlainHistoryCell { lines } +} + +pub(crate) fn new_active_exec_command( + command: Vec, + parsed: Vec, +) -> ExecCell { + new_exec_cell(command, parsed, None) +} + +pub(crate) fn new_completed_exec_command( + command: Vec, + parsed: Vec, + output: CommandOutput, +) -> ExecCell { + new_exec_cell(command, parsed, Some(output)) +} + +fn new_exec_cell( + command: Vec, + parsed: Vec, + output: Option, +) -> ExecCell { + ExecCell { + command, + parsed, + output, } } +fn exec_command_lines( + command: &[String], + parsed: &[ParsedCommand], + output: Option<&CommandOutput>, +) -> Vec> { + match parsed.is_empty() { + true => new_exec_command_generic(command, output), + false => new_parsed_command(parsed, output), + } +} + +fn new_parsed_command( + parsed_commands: &[ParsedCommand], + output: Option<&CommandOutput>, +) -> Vec> { + let mut lines: Vec = vec![match output { + None => Line::from("⚙︎ Working".magenta().bold()), + Some(o) if o.exit_code == 0 => Line::from("✓ Completed".green().bold()), + Some(o) => Line::from(format!("✗ Failed (exit {})", o.exit_code).red().bold()), + }]; + + for (i, parsed) in parsed_commands.iter().enumerate() { + let text = match parsed { + ParsedCommand::Read { name, .. } => format!("📖 {name}"), + ParsedCommand::ListFiles { cmd, path } => match path { + Some(p) => format!("📂 {p}"), + None => format!("📂 {}", shlex_join_safe(cmd)), + }, + ParsedCommand::Search { query, path, cmd } => match (query, path) { + (Some(q), Some(p)) => format!("🔎 {q} in {p}"), + (Some(q), None) => format!("🔎 {q}"), + (None, Some(p)) => format!("🔎 {p}"), + (None, None) => format!("🔎 {}", shlex_join_safe(cmd)), + }, + ParsedCommand::Format { .. } => "✨ Formatting".to_string(), + ParsedCommand::Test { cmd } => format!("🧪 {}", shlex_join_safe(cmd)), + ParsedCommand::Lint { cmd, .. } => format!("🧹 {}", shlex_join_safe(cmd)), + ParsedCommand::Unknown { cmd } => format!("⌨️ {}", shlex_join_safe(cmd)), + }; + + let first_prefix = if i == 0 { " └ " } else { " " }; + for (j, line_text) in text.lines().enumerate() { + let prefix = if j == 0 { first_prefix } else { " " }; + lines.push(Line::from(vec![ + Span::styled(prefix, Style::default().add_modifier(Modifier::DIM)), + Span::styled(line_text.to_string(), Style::default().fg(LIGHT_BLUE)), + ])); + } + } + + lines.extend(output_lines(output, true, false)); + lines.push(Line::from("")); + + lines +} + +fn new_exec_command_generic( + command: &[String], + output: Option<&CommandOutput>, +) -> Vec> { + let mut lines: Vec> = Vec::new(); + let command_escaped = strip_bash_lc_and_escape(command); + let mut cmd_lines = command_escaped.lines(); + if let Some(first) = cmd_lines.next() { + lines.push(Line::from(vec![ + "⚡ Running ".to_string().magenta(), + first.to_string().into(), + ])); + } else { + lines.push(Line::from("⚡ Running".to_string().magenta())); + } + for cont in cmd_lines { + lines.push(Line::from(cont.to_string())); + } + + lines.extend(output_lines(output, false, true)); + + lines +} + +pub(crate) fn new_active_mcp_tool_call(invocation: McpInvocation) -> PlainHistoryCell { + let title_line = Line::from(vec!["tool".magenta(), " running...".dim()]); + let lines: Vec = vec![ + title_line, + format_mcp_invocation(invocation.clone()), + Line::from(""), + ]; + + PlainHistoryCell { lines } +} + +/// If the first content is an image, return a new cell with the image. +/// TODO(rgwood-dd): Handle images properly even if they're not the first result. +fn try_new_completed_mcp_tool_call_with_image_output( + result: &Result, +) -> Option { + match result { + Ok(mcp_types::CallToolResult { content, .. }) => { + if let Some(mcp_types::ContentBlock::ImageContent(image)) = content.first() { + let raw_data = match base64::engine::general_purpose::STANDARD.decode(&image.data) { + Ok(data) => data, + Err(e) => { + error!("Failed to decode image data: {e}"); + return None; + } + }; + let reader = match ImageReader::new(Cursor::new(raw_data)).with_guessed_format() { + Ok(reader) => reader, + Err(e) => { + error!("Failed to guess image format: {e}"); + return None; + } + }; + + let image = match reader.decode() { + Ok(image) => image, + Err(e) => { + error!("Image decoding failed: {e}"); + return None; + } + }; + + Some(CompletedMcpToolCallWithImageOutput { _image: image }) + } else { + None + } + } + _ => None, + } +} + +pub(crate) fn new_completed_mcp_tool_call( + num_cols: usize, + invocation: McpInvocation, + duration: Duration, + success: bool, + result: Result, +) -> Box { + if let Some(cell) = try_new_completed_mcp_tool_call_with_image_output(&result) { + return Box::new(cell); + } + + let duration = format_duration(duration); + let status_str = if success { "success" } else { "failed" }; + let title_line = Line::from(vec![ + "tool".magenta(), + " ".into(), + if success { + status_str.green() + } else { + status_str.red() + }, + format!(", duration: {duration}").dim(), + ]); + + let mut lines: Vec> = Vec::new(); + lines.push(title_line); + lines.push(format_mcp_invocation(invocation)); + + match result { + Ok(mcp_types::CallToolResult { content, .. }) => { + if !content.is_empty() { + lines.push(Line::from("")); + + for tool_call_result in content { + let line_text = match tool_call_result { + mcp_types::ContentBlock::TextContent(text) => { + format_and_truncate_tool_result( + &text.text, + TOOL_CALL_MAX_LINES, + num_cols, + ) + } + mcp_types::ContentBlock::ImageContent(_) => { + // TODO show images even if they're not the first result, will require a refactor of `CompletedMcpToolCall` + "".to_string() + } + mcp_types::ContentBlock::AudioContent(_) => "
Commits
- ---- - ## Quickstart ### Installing and running Codex CLI @@ -99,607 +45,52 @@ Each archive contains a single entry with the platform baked into the name (e.g. ### Using Codex with your ChatGPT plan

- Codex CLI login + Codex CLI login

-Run `codex` and select **Sign in with ChatGPT**. You'll need a Plus, Pro, or Team ChatGPT account, and will get access to our latest models, including `gpt-5`, at no extra cost to your plan. (Enterprise is coming soon.) +Run `codex` and select **Sign in with ChatGPT**. We recommend signing into your ChatGPT account to use Codex as part of your Plus, Pro, Team, Edu, or Enterprise plan. [Learn more about what's included in your ChatGPT plan](https://help.openai.com/en/articles/11369540-codex-in-chatgpt). -> Important: If you've used the Codex CLI before, follow these steps to migrate from usage-based billing with your API key: -> -> 1. Update the CLI and ensure `codex --version` is `0.20.0` or later -> 2. Delete `~/.codex/auth.json` (this should be `C:\Users\USERNAME\.codex\auth.json` on Windows) -> 3. Run `codex login` again +You can also use Codex with an API key, but this requires [additional setup](./docs/authentication.md#usage-based-billing-alternative-use-an-openai-api-key). If you previously used an API key for usage-based billing, see the [migration steps](./docs/authentication.md#migrating-from-usage-based-billing-api-key). If you're having trouble with login, please comment on [this issue](https://github.com/openai/codex/issues/1243). -If you encounter problems with the login flow, please comment on [this issue](https://github.com/openai/codex/issues/1243). +### Model Context Protocol (MCP) -### Connecting on a "Headless" Machine +Codex CLI supports [MCP servers](./docs/advanced.md#model-context-protocol-mcp). Enable by adding an `mcp_servers` section to your `~/.codex/config.toml`. -Today, the login process entails running a server on `localhost:1455`. If you are on a "headless" server, such as a Docker container or are `ssh`'d into a remote machine, loading `localhost:1455` in the browser on your local machine will not automatically connect to the webserver running on the _headless_ machine, so you must use one of the following workarounds: -#### Authenticate locally and copy your credentials to the "headless" machine +### Configuration -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`). - -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" -``` - -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 -``` - -or try this one-liner: - -```shell -ssh user@remote 'mkdir -p ~/.codex && cat > ~/.codex/auth.json' < ~/.codex/auth.json -``` - -#### Connecting through VPS or remote - -If you run Codex on a remote machine (VPS/server) without a local browser, the login helper starts a server on `localhost:1455` on the remote host. To complete login in your local browser, forward that port to your machine before starting the login flow: - -```bash -# From your local machine -ssh -L 1455:localhost:1455 @ -``` - -Then, in that SSH session, run `codex` and select "Sign in with ChatGPT". When prompted, open the printed URL (it will be `http://localhost:1455/...`) in your local browser. The traffic will be tunneled to the remote server. - -### Usage-based billing alternative: Use an OpenAI API key - -If you prefer to pay-as-you-go, you can still authenticate with your OpenAI API key by setting it as an environment variable: - -```shell -export OPENAI_API_KEY="your-api-key-here" -``` - -Notes: - -- This command only sets the key for your current terminal session, which we recommend. To set it for all future sessions, you can also add the `export` line to your shell's configuration file (e.g., `~/.zshrc`). -- If you have signed in with ChatGPT, Codex will default to using your ChatGPT credits. If you wish to use your API key, use the `/logout` command to clear your ChatGPT authentication. - -#### Forcing a specific auth method (advanced) - -You can explicitly choose which authentication Codex should prefer when both are available. - -- To always use your API key (even when ChatGPT auth exists), set: - -```toml -# ~/.codex/config.toml -preferred_auth_method = "apikey" -``` - -Or override ad-hoc via CLI: - -```bash -codex --config preferred_auth_method="apikey" -``` - -- To prefer ChatGPT auth (default), set: - -```toml -# ~/.codex/config.toml -preferred_auth_method = "chatgpt" -``` - -Notes: - -- When `preferred_auth_method = "apikey"` and an API key is available, the login screen is skipped. -- When `preferred_auth_method = "chatgpt"` (default), Codex prefers ChatGPT auth if present; if only an API key is present, it will use the API key. Certain account types may also require API-key mode. - -### Choosing Codex's level of autonomy - -We always recommend running Codex in its default sandbox that gives you strong guardrails around what the agent can do. The default sandbox prevents it from editing files outside its workspace, or from accessing the network. - -When you launch Codex in a new folder, it detects whether the folder is version controlled and recommends one of two levels of autonomy: - -#### **1. Read/write** - -- Codex can run commands and write files in the workspace without approval. -- To write files in other folders, access network, update git or perform other actions protected by the sandbox, Codex will need your permission. -- By default, the workspace includes the current directory, as well as temporary directories like `/tmp`. You can see what directories are in the workspace with the `/status` command. See the docs for how to customize this behavior. -- Advanced: You can manually specify this configuration by running `codex --sandbox workspace-write --ask-for-approval on-request` -- This is the recommended default for version-controlled folders. - -#### **2. Read-only** - -- Codex can run read-only commands without approval. -- To edit files, access network, or perform other actions protected by the sandbox, Codex will need your permission. -- Advanced: You can manually specify this configuration by running `codex --sandbox read-only --ask-for-approval on-request` -- This is the recommended default non-version-controlled folders. - -#### **3. Advanced configuration** - -Codex gives you fine-grained control over the sandbox with the `--sandbox` option, and over when it requests approval with the `--ask-for-approval` option. Run `codex help` for more on these options. - -#### Can I run without ANY approvals? - -Yes, run codex non-interactively with `--ask-for-approval never`. This option works with all `--sandbox` options, so you still have full control over Codex's level of autonomy. It will make its best attempt with whatever contrainsts you provide. For example: - -- Use `codex --ask-for-approval never --sandbox read-only` when you are running many agents to answer questions in parallel in the same workspace. -- Use `codex --ask-for-approval never --sandbox workspace-write` when you want the agent to non-interactively take time to produce the best outcome, with strong guardrails around its behavior. -- Use `codex --ask-for-approval never --sandbox danger-full-access` to dangerously give the agent full autonomy. Because this disables important safety mechanisms, we recommend against using this unless running Codex in an isolated environment. - -#### Fine-tuning in `config.toml` - -```toml -# approval mode -approval_policy = "untrusted" -sandbox_mode = "read-only" - -# full-auto mode -approval_policy = "on-request" -sandbox_mode = "workspace-write" - -# Optional: allow network in workspace-write mode -[sandbox_workspace_write] -network_access = true -``` - -You can also save presets as **profiles**: - -```toml -[profiles.full_auto] -approval_policy = "on-request" -sandbox_mode = "workspace-write" - -[profiles.readonly_quiet] -approval_policy = "never" -sandbox_mode = "read-only" -``` - -### Example prompts - -Below are a few bite-size examples you can copy-paste. Replace the text in quotes with your own task. See the [prompting guide](https://github.com/openai/codex/blob/main/codex-cli/examples/prompting_guide.md) for more tips and usage patterns. - -| ✨ | What you type | What happens | -| --- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -| 1 | `codex "Refactor the Dashboard component to React Hooks"` | Codex rewrites the class component, runs `npm test`, and shows the diff. | -| 2 | `codex "Generate SQL migrations for adding a users table"` | Infers your ORM, creates migration files, and runs them in a sandboxed DB. | -| 3 | `codex "Write unit tests for utils/date.ts"` | Generates tests, executes them, and iterates until they pass. | -| 4 | `codex "Bulk-rename *.jpeg -> *.jpg with git mv"` | Safely renames files and updates imports/usages. | -| 5 | `codex "Explain what this regex does: ^(?=.*[A-Z]).{8,}$"` | Outputs a step-by-step human explanation. | -| 6 | `codex "Carefully review this repo, and propose 3 high impact well-scoped PRs"` | Suggests impactful PRs in the current codebase. | -| 7 | `codex "Look for vulnerabilities and create a security review report"` | Finds and explains security bugs. | - -## Running with a prompt as input - -You can also run Codex CLI with a prompt as input: - -```shell -codex "explain this codebase to me" -``` - -```shell -codex --full-auto "create the fanciest todo-list app" -``` - -That's it - Codex will scaffold a file, run it inside a sandbox, install any -missing dependencies, and show you the live result. Approve the changes and -they'll be committed to your working directory. - -## Using Open Source Models - -
-Use --profile to use other models - -Codex also allows you to use other providers that support the OpenAI Chat Completions (or Responses) API. - -To do so, you must first define custom [providers](./config.md#model_providers) in `~/.codex/config.toml`. For example, the provider for a standard Ollama setup would be defined as follows: - -```toml -[model_providers.ollama] -name = "Ollama" -base_url = "http://localhost:11434/v1" -``` - -The `base_url` will have `/chat/completions` appended to it to build the full URL for the request. - -For providers that also require an `Authorization` header of the form `Bearer: SECRET`, an `env_key` can be specified, which indicates the environment variable to read to use as the value of `SECRET` when making a request: - -```toml -[model_providers.openrouter] -name = "OpenRouter" -base_url = "https://openrouter.ai/api/v1" -env_key = "OPENROUTER_API_KEY" -``` - -Providers that speak the Responses API are also supported by adding `wire_api = "responses"` as part of the definition. Accessing OpenAI models via Azure is an example of such a provider, though it also requires specifying additional `query_params` that need to be appended to the request URL: - -```toml -[model_providers.azure] -name = "Azure" -# Make sure you set the appropriate subdomain for this URL. -base_url = "https://YOUR_PROJECT_NAME.openai.azure.com/openai" -env_key = "AZURE_OPENAI_API_KEY" # Or "OPENAI_API_KEY", whichever you use. -# Newer versions appear to support the responses API, see https://github.com/openai/codex/pull/1321 -query_params = { api-version = "2025-04-01-preview" } -wire_api = "responses" -``` - -Once you have defined a provider you wish to use, you can configure it as your default provider as follows: - -```toml -model_provider = "azure" -``` - -> [!TIP] -> If you find yourself experimenting with a variety of models and providers, then you likely want to invest in defining a _profile_ for each configuration like so: - -```toml -[profiles.o3] -model_provider = "azure" -model = "o3" - -[profiles.mistral] -model_provider = "ollama" -model = "mistral" -``` - -This way, you can specify one command-line argument (.e.g., `--profile o3`, `--profile mistral`) to override multiple settings together. - -
- -Codex can run fully locally against an OpenAI-compatible OSS host (like Ollama) using the `--oss` flag: - -- Interactive UI: - - codex --oss -- Non-interactive (programmatic) mode: - - echo "Refactor utils" | codex exec --oss - -Model selection when using `--oss`: - -- If you omit `-m/--model`, Codex defaults to -m gpt-oss:20b and will verify it exists locally (downloading if needed). -- To pick a different size, pass one of: - - -m "gpt-oss:20b" - - -m "gpt-oss:120b" - -Point Codex at your own OSS host: - -- By default, `--oss` talks to http://localhost:11434/v1. -- To use a different host, set one of these environment variables before running Codex: - - CODEX_OSS_BASE_URL, for example: - - CODEX_OSS_BASE_URL="http://my-ollama.example.com:11434/v1" codex --oss -m gpt-oss:20b - - or CODEX_OSS_PORT (when the host is localhost): - - CODEX_OSS_PORT=11434 codex --oss - -Advanced: you can persist this in your config instead of environment variables by overriding the built-in `oss` provider in `~/.codex/config.toml`: - -```toml -[model_providers.oss] -name = "Open Source" -base_url = "http://my-ollama.example.com:11434/v1" -``` +Codex CLI supports a rich set of configuration options, with preferences stored in `~/.codex/config.toml`. For full configuration options, see [Configuration](./docs/config.md). --- -### Platform sandboxing details - -By default, Codex CLI runs code and shell commands inside a restricted sandbox to protect your system. - -> [!IMPORTANT] -> Not all tool calls are sandboxed. Specifically, **trusted Model Context Protocol (MCP) tool calls** are executed outside of the sandbox. -> This is intentional: MCP tools are explicitly configured and trusted by you, and they often need to connect to **external applications or services** (e.g. issue trackers, databases, messaging systems). -> Running them outside the sandbox allows Codex to integrate with these external systems without being blocked by sandbox restrictions. - -The mechanism Codex uses to implement the sandbox policy depends on your OS: - -- **macOS 12+** uses **Apple Seatbelt** and runs commands using `sandbox-exec` with a profile (`-p`) that corresponds to the `--sandbox` that was specified. -- **Linux** uses a combination of Landlock/seccomp APIs to enforce the `sandbox` configuration. - -Note that when running Linux in a containerized environment such as Docker, sandboxing may not work if the host/container configuration does not support the necessary Landlock/seccomp APIs. In such cases, we recommend configuring your Docker container so that it provides the sandbox guarantees you are looking for and then running `codex` with `--sandbox danger-full-access` (or, more simply, the `--dangerously-bypass-approvals-and-sandbox` flag) within your container. - ---- - -## Experimental technology disclaimer - -Codex CLI is an experimental project under active development. It is not yet stable, may contain bugs, incomplete features, or undergo breaking changes. We're building it in the open with the community and welcome: - -- Bug reports -- Feature requests -- Pull requests -- Good vibes - -Help us improve by filing issues or submitting PRs (see the section below for how to contribute)! - ---- - -## System requirements - -| Requirement | Details | -| --------------------------- | --------------------------------------------------------------- | -| Operating systems | macOS 12+, Ubuntu 20.04+/Debian 10+, or Windows 11 **via WSL2** | -| Git (optional, recommended) | 2.23+ for built-in PR helpers | -| RAM | 4-GB minimum (8-GB recommended) | - ---- - -## CLI reference - -| Command | Purpose | Example | -| ------------------ | ---------------------------------- | ------------------------------- | -| `codex` | Interactive TUI | `codex` | -| `codex "..."` | Initial prompt for interactive TUI | `codex "fix lint errors"` | -| `codex exec "..."` | Non-interactive "automation mode" | `codex exec "explain utils.ts"` | - -Key flags: `--model/-m`, `--ask-for-approval/-a`. - ---- - -## Memory & project docs - -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 -2. `AGENTS.md` at repo root - shared project notes -3. `AGENTS.md` in the current working directory - sub-folder/feature specifics - ---- - -## Non-interactive / CI mode - -Run Codex head-less in pipelines. Example GitHub Action step: - -```yaml -- name: Update changelog via Codex - run: | - npm install -g @openai/codex - export OPENAI_API_KEY="${{ secrets.OPENAI_KEY }}" - codex exec --full-auto "update CHANGELOG for next release" -``` - -## Model Context Protocol (MCP) - -The Codex CLI can be configured to leverage MCP servers by defining an [`mcp_servers`](./codex-rs/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.: - -```toml -# IMPORTANT: the top-level key is `mcp_servers` rather than `mcpServers`. -[mcp_servers.server-name] -command = "npx" -args = ["-y", "mcp-server"] -env = { "API_KEY" = "value" } -``` - -> [!TIP] -> It is somewhat experimental, but the Codex CLI can also be run as an MCP _server_ via `codex mcp`. If you launch it with an MCP client such as `npx @modelcontextprotocol/inspector codex mcp` and send it a `tools/list` request, you will see that there is only one tool, `codex`, that accepts a grab-bag of inputs, including a catch-all `config` map for anything you might want to override. Feel free to play around with it and provide feedback via GitHub issues. - -## Tracing / verbose logging - -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: - -``` -tail -F ~/.codex/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. - -See the Rust documentation on [`RUST_LOG`](https://docs.rs/env_logger/latest/env_logger/#enabling-logging) for more information on the configuration options. - ---- - -### DotSlash - -The GitHub Release also contains a [DotSlash](https://dotslash-cli.com/) file for the Codex CLI named `codex`. Using a DotSlash file makes it possible to make a lightweight commit to source control to ensure all contributors use the same version of an executable, regardless of what platform they use for development. - - - -
-Build from source - -```bash -# Clone the repository and navigate to the root of the Cargo workspace. -git clone https://github.com/openai/codex.git -cd codex/codex-rs - -# Install the Rust toolchain, if necessary. -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -source "$HOME/.cargo/env" -rustup component add rustfmt -rustup component add clippy - -# Build Codex. -cargo build - -# Launch the TUI with a sample prompt. -cargo run --bin codex -- "explain this codebase to me" - -# After making changes, ensure the code is clean. -cargo fmt -- --config imports_granularity=Item -cargo clippy --tests - -# Run the tests. -cargo test -``` - -
- ---- - -## Configuration - -Codex supports a rich set of configuration options documented in [`codex-rs/config.md`](./codex-rs/config.md). - -By default, Codex loads its configuration from `~/.codex/config.toml`. - -Though `--config` can be used to set/override ad-hoc config values for individual invocations of `codex`. - ---- - -## FAQ - -
-OpenAI released a model called Codex in 2021 - is this related? - -In 2021, OpenAI released Codex, an AI system designed to generate code from natural language prompts. That original Codex model was deprecated as of March 2023 and is separate from the CLI tool. - -
- -
-Which models are supported? - -Any model available with [Responses API](https://platform.openai.com/docs/api-reference/responses). The default is `o4-mini`, but pass `--model gpt-4.1` or set `model: gpt-4.1` in your config file to override. - -
-
-Why does o3 or o4-mini not work for me? - -It's possible that your [API account needs to be verified](https://help.openai.com/en/articles/10910291-api-organization-verification) in order to start streaming responses and seeing chain of thought summaries from the API. If you're still running into issues, please let us know! - -
- -
-How do I stop Codex from editing my files? - -Codex runs model-generated commands in a sandbox. If a proposed command or file change doesn't look right, you can simply type **n** to deny the command or give the model feedback. - -
-
-Does it work on Windows? - -Not directly. It requires [Windows Subsystem for Linux (WSL2)](https://learn.microsoft.com/en-us/windows/wsl/install) - Codex has been tested on macOS and Linux with Node 22. - -
- ---- - -## Zero data retention (ZDR) usage - -Codex CLI **does** support OpenAI organizations with [Zero Data Retention (ZDR)](https://platform.openai.com/docs/guides/your-data#zero-data-retention) enabled. If your OpenAI organization has Zero Data Retention enabled and you still encounter errors such as: - -``` -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: - -```toml -disable_response_storage = true -``` - -See [the configuration documentation on `disable_response_storage`](./codex-rs/config.md#disable_response_storage) for details. - ---- - -## Codex open source fund - -We're excited to launch a **$1 million initiative** supporting open source projects that use Codex CLI and other OpenAI models. - -- Grants are awarded up to **$25,000** API credits. -- Applications are reviewed **on a rolling basis**. - -**Interested? [Apply here](https://openai.com/form/codex-open-source-fund/).** - ---- - -## Contributing - -This project is under active development and the code will likely change pretty significantly. - -**At the moment, we only plan to prioritize reviewing external contributions for bugs or security fixes.** - -If you want to add a new feature or change the behavior of an existing one, please open an issue proposing the feature and get approval from an OpenAI team member before spending time building it. - -**New contributions that don't go through this process may be closed** if they aren't aligned with our current roadmap or conflict with other priorities/upcoming features. - -### Development workflow - -- Create a _topic branch_ from `main` - e.g. `feat/interactive-prompt`. -- Keep your changes focused. Multiple unrelated fixes should be opened as separate PRs. -- Following the [development setup](#development-workflow) instructions above, ensure your change is free of lint warnings and test failures. - -### Writing high-impact code changes - -1. **Start with an issue.** Open a new one or comment on an existing discussion so we can agree on the solution before code is written. -2. **Add or update tests.** Every new feature or bug-fix should come with test coverage that fails before your change and passes afterwards. 100% coverage is not required, but aim for meaningful assertions. -3. **Document behaviour.** If your change affects user-facing behaviour, update the README, inline help (`codex --help`), or relevant example projects. -4. **Keep commits atomic.** Each commit should compile and the tests should pass. This makes reviews and potential rollbacks easier. - -### Opening a pull request - -- Fill in the PR template (or include similar information) - **What? Why? How?** -- Run **all** checks locally (`cargo test && cargo clippy --tests && cargo fmt -- --config imports_granularity=Item`). CI failures that could have been caught locally slow down the process. -- Make sure your branch is up-to-date with `main` and that you have resolved merge conflicts. -- Mark the PR as **Ready for review** only when you believe it is in a merge-able state. - -### Review process - -1. One maintainer will be assigned as a primary reviewer. -2. If your PR adds a new feature that was not previously discussed and approved, we may choose to close your PR (see [Contributing](#contributing)). -3. We may ask for changes - please do not take this personally. We value the work, but we also value consistency and long-term maintainability. -5. When there is consensus that the PR meets the bar, a maintainer will squash-and-merge. - -### Community values - -- **Be kind and inclusive.** Treat others with respect; we follow the [Contributor Covenant](https://www.contributor-covenant.org/). -- **Assume good intent.** Written communication is hard - err on the side of generosity. -- **Teach & learn.** If you spot something confusing, open an issue or PR with improvements. - -### Getting help - -If you run into problems setting up the project, would like feedback on an idea, or just want to say _hi_ - please open a Discussion or jump into the relevant issue. We are happy to help. - -Together we can make Codex CLI an incredible tool. **Happy hacking!** :rocket: - -### Contributor license agreement (CLA) - -All contributors **must** accept the CLA. The process is lightweight: - -1. Open your pull request. -2. Paste the following comment (or reply `recheck` if you've signed before): - - ```text - I have read the CLA Document and I hereby sign the CLA - ``` - -3. The CLA-Assistant bot records your signature in the repo and marks the status check as passed. - -No special Git commands, email attachments, or commit footers required. - -#### Quick fixes - -| Scenario | Command | -| ----------------- | ------------------------------------------------ | -| Amend last commit | `git commit --amend -s --no-edit && git push -f` | - -The **DCO check** blocks merges until every commit in the PR carries the footer (with squash this is just the one). - -### Releasing `codex` - -_For admins only._ - -Make sure you are on `main` and have no local changes. Then run: - -```shell -VERSION=0.2.0 # Can also be 0.2.0-alpha.1 or any valid Rust version. -./codex-rs/scripts/create_github_release.sh "$VERSION" -``` - -This will make a local commit on top of `main` with `version` set to `$VERSION` in `codex-rs/Cargo.toml` (note that on `main`, we leave the version as `version = "0.0.0"`). - -This will push the commit using the tag `rust-v${VERSION}`, which in turn kicks off [the release workflow](.github/workflows/rust-release.yml). This will create a new GitHub Release named `$VERSION`. - -If everything looks good in the generated GitHub Release, uncheck the **pre-release** box so it is the latest release. - -Create a PR to update [`Formula/c/codex.rb`](https://github.com/Homebrew/homebrew-core/blob/main/Formula/c/codex.rb) on Homebrew. - ---- - -## Security & responsible AI - -Have you discovered a vulnerability or have concerns about model output? Please e-mail **security@openai.com** and we will respond promptly. +### Docs & FAQ + +- [**Getting started**](./docs/getting-started.md) + - [CLI usage](./docs/getting-started.md#cli-usage) + - [Running with a prompt as input](./docs/getting-started.md#running-with-a-prompt-as-input) + - [Example prompts](./docs/getting-started.md#example-prompts) + - [Memory with AGENTS.md](./docs/getting-started.md#memory--project-docs) + - [Configuration](./docs/config.md) +- [**Sandbox & approvals**](./docs/sandbox.md) +- [**Authentication**](./docs/authentication.md) + - [Auth methods](./docs/authentication.md#forcing-a-specific-auth-method-advanced) + - [Login on a "Headless" machine](./docs/authentication.md#connecting-on-a-headless-machine) +- [**Advanced**](./docs/advanced.md) + - [Non-interactive / CI mode](./docs/advanced.md#non-interactive--ci-mode) + - [Tracing / verbose logging](./docs/advanced.md#tracing--verbose-logging) + - [Model Context Protocol (MCP)](./docs/advanced.md#model-context-protocol-mcp) +- [**Zero data retention (ZDR)**](./docs/zdr.md) +- [**Contributing**](./docs/contributing.md) +- [**Install & build**](./docs/install.md) + - [System Requirements](./docs/install.md#system-requirements) + - [DotSlash](./docs/install.md#dotslash) + - [Build from source](./docs/install.md#build-from-source) +- [**FAQ**](./docs/faq.md) +- [**Open source fund**](./docs/open-source-fund.md) --- ## License This repository is licensed under the [Apache-2.0 License](LICENSE). + diff --git a/codex-rs/README.md b/codex-rs/README.md index e74cdfc2cd..390f5d31aa 100644 --- a/codex-rs/README.md +++ b/codex-rs/README.md @@ -19,11 +19,11 @@ While we are [working to close the gap between the TypeScript and Rust implement ### Config -Codex supports a rich set of configuration options. Note that the Rust CLI uses `config.toml` instead of `config.json`. See [`config.md`](./config.md) for details. +Codex supports a rich set of configuration options. Note that the Rust CLI uses `config.toml` instead of `config.json`. See [`docs/config.md`](../docs/config.md) for details. ### Model Context Protocol Support -Codex CLI functions as an MCP client that can connect to MCP servers on startup. See the [`mcp_servers`](./config.md#mcp_servers) section in the configuration documentation for details. +Codex CLI functions as an MCP client that can connect to MCP servers on startup. See the [`mcp_servers`](../docs/config.md#mcp_servers) section in the configuration documentation for details. It is still experimental, but you can also launch Codex as an MCP _server_ by running `codex mcp`. Use the [`@modelcontextprotocol/inspector`](https://github.com/modelcontextprotocol/inspector) to try it out: @@ -33,7 +33,7 @@ npx @modelcontextprotocol/inspector codex mcp ### Notifications -You can enable notifications by configuring a script that is run whenever the agent finishes a turn. The [notify documentation](./config.md#notify) includes a detailed example that explains how to get desktop notifications via [terminal-notifier](https://github.com/julienXX/terminal-notifier) on macOS. +You can enable notifications by configuring a script that is run whenever the agent finishes a turn. The [notify documentation](../docs/config.md#notify) includes a detailed example that explains how to get desktop notifications via [terminal-notifier](https://github.com/julienXX/terminal-notifier) on macOS. ### `codex exec` to run Codex programmatially/non-interactively diff --git a/docs/advanced.md b/docs/advanced.md new file mode 100644 index 0000000000..26f735991f --- /dev/null +++ b/docs/advanced.md @@ -0,0 +1,42 @@ +## Advanced + +## Non-interactive / CI mode + +Run Codex head-less in pipelines. Example GitHub Action step: + +```yaml +- name: Update changelog via Codex + run: | + npm install -g @openai/codex + export OPENAI_API_KEY="${{ secrets.OPENAI_KEY }}" + codex exec --full-auto "update CHANGELOG for next release" +``` + +## Tracing / verbose logging + +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: + +``` +tail -F ~/.codex/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. + +See the Rust documentation on [`RUST_LOG`](https://docs.rs/env_logger/latest/env_logger/#enabling-logging) for more information on the configuration options. + +## 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.: + +```toml +# IMPORTANT: the top-level key is `mcp_servers` rather than `mcpServers`. +[mcp_servers.server-name] +command = "npx" +args = ["-y", "mcp-server"] +env = { "API_KEY" = "value" } +``` + +> [!TIP] +> It is somewhat experimental, but the Codex CLI can also be run as an MCP _server_ via `codex mcp`. If you launch it with an MCP client such as `npx @modelcontextprotocol/inspector codex mcp` and send it a `tools/list` request, you will see that there is only one tool, `codex`, that accepts a grab-bag of inputs, including a catch-all `config` map for anything you might want to override. Feel free to play around with it and provide feedback via GitHub issues. \ No newline at end of file diff --git a/docs/authentication.md b/docs/authentication.md new file mode 100644 index 0000000000..d318be708c --- /dev/null +++ b/docs/authentication.md @@ -0,0 +1,88 @@ +# Authentication + +## Usage-based billing alternative: Use an OpenAI API key + +If you prefer to pay-as-you-go, you can still authenticate with your OpenAI API key by setting it as an environment variable: + +```shell +export OPENAI_API_KEY="your-api-key-here" +``` + +## Migrating to ChatGPT login from API key + +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`) +3. Run `codex login` again + +## Forcing a specific auth method (advanced) + +You can explicitly choose which authentication Codex should prefer when both are available. + +- To always use your API key (even when ChatGPT auth exists), set: + +```toml +# ~/.codex/config.toml +preferred_auth_method = "apikey" +``` + +Or override ad-hoc via CLI: + +```bash +codex --config preferred_auth_method="apikey" +``` + +- To prefer ChatGPT auth (default), set: + +```toml +# ~/.codex/config.toml +preferred_auth_method = "chatgpt" +``` + +Notes: + +- When `preferred_auth_method = "apikey"` and an API key is available, the login screen is skipped. +- When `preferred_auth_method = "chatgpt"` (default), Codex prefers ChatGPT auth if present; if only an API key is present, it will use the API key. Certain account types may also require API-key mode. +- To check which auth method is being used during a session, use the `/status` command in the TUI. + +## Connecting on a "Headless" Machine + +Today, the login process entails running a server on `localhost:1455`. If you are on a "headless" server, such as a Docker container or are `ssh`'d into a remote machine, loading `localhost:1455` in the browser on your local machine will not automatically connect to the webserver running on the _headless_ machine, so you must use one of the following workarounds: + +### 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`). + +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" +``` + +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 +``` + +or try this one-liner: + +```shell +ssh user@remote 'mkdir -p ~/.codex && cat > ~/.codex/auth.json' < ~/.codex/auth.json +``` + +### Connecting through VPS or remote + +If you run Codex on a remote machine (VPS/server) without a local browser, the login helper starts a server on `localhost:1455` on the remote host. To complete login in your local browser, forward that port to your machine before starting the login flow: + +```bash +# From your local machine +ssh -L 1455:localhost:1455 @ +``` + +Then, in that SSH session, run `codex` and select "Sign in with ChatGPT". When prompted, open the printed URL (it will be `http://localhost:1455/...`) in your local browser. The traffic will be tunneled to the remote server. \ No newline at end of file diff --git a/codex-rs/config.md b/docs/config.md similarity index 82% rename from codex-rs/config.md rename to docs/config.md index a9d01dbc26..7d0bd7d2b8 100644 --- a/codex-rs/config.md +++ b/docs/config.md @@ -1,5 +1,6 @@ # Config + Codex supports several mechanisms for setting config values: - Config-specific command-line flags, such as `--model o3` (highest precedence). @@ -391,9 +392,9 @@ include_only = ["PATH", "HOME"] | ------------------------- | -------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `inherit` | string | `all` | Starting template for the environment:
`all` (clone full parent env), `core` (`HOME`, `PATH`, `USER`, …), or `none` (start empty). | | `ignore_default_excludes` | boolean | `false` | When `false`, Codex removes any var whose **name** contains `KEY`, `SECRET`, or `TOKEN` (case-insensitive) before other rules run. | -| `exclude` | array<string> | `[]` | Case-insensitive glob patterns to drop after the default filter.
Examples: `"AWS_*"`, `"AZURE_*"`. | -| `set` | table<string,string> | `{}` | Explicit key/value overrides or additions – always win over inherited values. | -| `include_only` | array<string> | `[]` | If non-empty, a whitelist of patterns; only variables that match _one_ pattern survive the final step. (Generally used with `inherit = "all"`.) | +| `exclude` | array | `[]` | Case-insensitive glob patterns to drop after the default filter.
Examples: `"AWS_*"`, `"AZURE_*"`. | +| `set` | table | `{}` | Explicit key/value overrides or additions – always win over inherited values. | +| `include_only` | array | `[]` | If non-empty, a whitelist of patterns; only variables that match _one_ pattern survive the final step. (Generally used with `inherit = "all"`.) | The patterns are **glob style**, not full regular expressions: `*` matches any number of characters, `?` matches exactly one, and character classes like @@ -562,3 +563,55 @@ Options that are specific to the TUI. [tui] # More to come here ``` + +## Config reference + +| Key | Type / Values | Notes | +| --- | --- | --- | +| `model` | string | Model to use (e.g., `gpt-5`). | +| `model_provider` | string | Provider id from `model_providers` (default: `openai`). | +| `model_context_window` | number | Context window tokens. | +| `model_max_output_tokens` | number | Max output tokens. | +| `approval_policy` | `untrusted` | `on-failure` | `on-request` | `never` | When to prompt for approval. | +| `sandbox_mode` | `read-only` | `workspace-write` | `danger-full-access` | OS sandbox policy. | +| `sandbox_workspace_write.writable_roots` | array | Extra writable roots in workspace‑write. | +| `sandbox_workspace_write.network_access` | boolean | Allow network in workspace‑write (default: false). | +| `sandbox_workspace_write.exclude_tmpdir_env_var` | boolean | Exclude `$TMPDIR` from writable roots (default: false). | +| `sandbox_workspace_write.exclude_slash_tmp` | boolean | Exclude `/tmp` from writable roots (default: false). | +| `disable_response_storage` | boolean | Required for ZDR orgs. | +| `notify` | array | External program for notifications. | +| `instructions` | string | Currently ignored; use `experimental_instructions_file` or `AGENTS.md`. | +| `mcp_servers..command` | string | MCP server launcher command. | +| `mcp_servers..args` | array | MCP server args. | +| `mcp_servers..env` | map | MCP server env vars. | +| `model_providers..name` | string | Display name. | +| `model_providers..base_url` | string | API base URL. | +| `model_providers..env_key` | string | Env var for API key. | +| `model_providers..wire_api` | `chat` | `responses` | Protocol used (default: `chat`). | +| `model_providers..query_params` | map | Extra query params (e.g., Azure `api-version`). | +| `model_providers..http_headers` | map | Additional static headers. | +| `model_providers..env_http_headers` | map | Headers sourced from env vars. | +| `model_providers..request_max_retries` | number | Per‑provider HTTP retry count (default: 4). | +| `model_providers..stream_max_retries` | number | SSE stream retry count (default: 5). | +| `model_providers..stream_idle_timeout_ms` | number | SSE idle timeout (ms) (default: 300000). | +| `project_doc_max_bytes` | number | Max bytes to read from `AGENTS.md`. | +| `profile` | string | Active profile name. | +| `profiles..*` | various | Profile‑scoped overrides of the same keys. | +| `history.persistence` | `save-all` | `none` | History file persistence (default: `save-all`). | +| `history.max_bytes` | number | Currently ignored (not enforced). | +| `file_opener` | `vscode` | `vscode-insiders` | `windsurf` | `cursor` | `none` | URI scheme for clickable citations (default: `vscode`). | +| `tui` | table | TUI‑specific options (reserved). | +| `hide_agent_reasoning` | boolean | Hide model reasoning events. | +| `show_raw_agent_reasoning` | boolean | Show raw reasoning (when available). | +| `model_reasoning_effort` | `minimal` | `low` | `medium` | `high` | Responses API reasoning effort. | +| `model_reasoning_summary` | `auto` | `concise` | `detailed` | `none` | Reasoning summaries. | +| `model_verbosity` | `low` | `medium` | `high` | GPT‑5 text verbosity (Responses API). | +| `model_supports_reasoning_summaries` | boolean | Force‑enable reasoning summaries. | +| `chatgpt_base_url` | string | Base URL for ChatGPT auth flow. | +| `experimental_resume` | string (path) | Resume JSONL path (internal/experimental). | +| `experimental_instructions_file` | string (path) | Replace built‑in instructions (experimental). | +| `experimental_use_exec_command_tool` | boolean | Use experimental exec command tool. | +| `responses_originator_header_internal_override` | string | Override `originator` header value. | +| `projects..trust_level` | string | Mark project/worktree as trusted (only `"trusted"` is recognized). | +| `preferred_auth_method` | `chatgpt` | `apikey` | Select default auth method (default: `chatgpt`). | +| `tools.web_search` | boolean | Enable web search tool (alias: `web_search_request`) (default: false). | diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000000..1ad681e94d --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,94 @@ +## Contributing + +This project is under active development and the code will likely change pretty significantly. + +**At the moment, we only plan to prioritize reviewing external contributions for bugs or security fixes.** + +If you want to add a new feature or change the behavior of an existing one, please open an issue proposing the feature and get approval from an OpenAI team member before spending time building it. + +**New contributions that don't go through this process may be closed** if they aren't aligned with our current roadmap or conflict with other priorities/upcoming features. + +### Development workflow + +- Create a _topic branch_ from `main` - e.g. `feat/interactive-prompt`. +- Keep your changes focused. Multiple unrelated fixes should be opened as separate PRs. +- Following the [development setup](#development-workflow) instructions above, ensure your change is free of lint warnings and test failures. + +### Writing high-impact code changes + +1. **Start with an issue.** Open a new one or comment on an existing discussion so we can agree on the solution before code is written. +2. **Add or update tests.** Every new feature or bug-fix should come with test coverage that fails before your change and passes afterwards. 100% coverage is not required, but aim for meaningful assertions. +3. **Document behaviour.** If your change affects user-facing behaviour, update the README, inline help (`codex --help`), or relevant example projects. +4. **Keep commits atomic.** Each commit should compile and the tests should pass. This makes reviews and potential rollbacks easier. + +### Opening a pull request + +- Fill in the PR template (or include similar information) - **What? Why? How?** +- Run **all** checks locally (`cargo test && cargo clippy --tests && cargo fmt -- --config imports_granularity=Item`). CI failures that could have been caught locally slow down the process. +- Make sure your branch is up-to-date with `main` and that you have resolved merge conflicts. +- Mark the PR as **Ready for review** only when you believe it is in a merge-able state. + +### Review process + +1. One maintainer will be assigned as a primary reviewer. +2. If your PR adds a new feature that was not previously discussed and approved, we may choose to close your PR (see [Contributing](#contributing)). +3. We may ask for changes - please do not take this personally. We value the work, but we also value consistency and long-term maintainability. +5. When there is consensus that the PR meets the bar, a maintainer will squash-and-merge. + +### Community values + +- **Be kind and inclusive.** Treat others with respect; we follow the [Contributor Covenant](https://www.contributor-covenant.org/). +- **Assume good intent.** Written communication is hard - err on the side of generosity. +- **Teach & learn.** If you spot something confusing, open an issue or PR with improvements. + +### Getting help + +If you run into problems setting up the project, would like feedback on an idea, or just want to say _hi_ - please open a Discussion or jump into the relevant issue. We are happy to help. + +Together we can make Codex CLI an incredible tool. **Happy hacking!** :rocket: + +### Contributor license agreement (CLA) + +All contributors **must** accept the CLA. The process is lightweight: + +1. Open your pull request. +2. Paste the following comment (or reply `recheck` if you've signed before): + + ```text + I have read the CLA Document and I hereby sign the CLA + ``` + +3. The CLA-Assistant bot records your signature in the repo and marks the status check as passed. + +No special Git commands, email attachments, or commit footers required. + +#### Quick fixes + +| Scenario | Command | +| ----------------- | ------------------------------------------------ | +| Amend last commit | `git commit --amend -s --no-edit && git push -f` | + +The **DCO check** blocks merges until every commit in the PR carries the footer (with squash this is just the one). + +### Releasing `codex` + +_For admins only._ + +Make sure you are on `main` and have no local changes. Then run: + +```shell +VERSION=0.2.0 # Can also be 0.2.0-alpha.1 or any valid Rust version. +./codex-rs/scripts/create_github_release.sh "$VERSION" +``` + +This will make a local commit on top of `main` with `version` set to `$VERSION` in `codex-rs/Cargo.toml` (note that on `main`, we leave the version as `version = "0.0.0"`). + +This will push the commit using the tag `rust-v${VERSION}`, which in turn kicks off [the release workflow](../.github/workflows/rust-release.yml). This will create a new GitHub Release named `$VERSION`. + +If everything looks good in the generated GitHub Release, uncheck the **pre-release** box so it is the latest release. + +Create a PR to update [`Formula/c/codex.rb`](https://github.com/Homebrew/homebrew-core/blob/main/Formula/c/codex.rb) on Homebrew. + +### Security & responsible AI + +Have you discovered a vulnerability or have concerns about model output? Please e-mail **security@openai.com** and we will respond promptly. \ No newline at end of file diff --git a/docs/experimental.md b/docs/experimental.md new file mode 100644 index 0000000000..23d31c78c7 --- /dev/null +++ b/docs/experimental.md @@ -0,0 +1,10 @@ +## Experimental technology disclaimer + +Codex CLI is an experimental project under active development. It is not yet stable, may contain bugs, incomplete features, or undergo breaking changes. We're building it in the open with the community and welcome: + +- Bug reports +- Feature requests +- Pull requests +- Good vibes + +Help us improve by filing issues or submitting PRs (see the section below for how to contribute)! \ No newline at end of file diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000000..6f192fed12 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,23 @@ +## FAQ + +### OpenAI released a model called Codex in 2021 - is this related? + +In 2021, OpenAI released Codex, an AI system designed to generate code from natural language prompts. That original Codex model was deprecated as of March 2023 and is separate from the CLI tool. + +### Which models are supported? + +We recommend using Codex with GPT-5, our best coding model. The default reasoning level is medium, and you can upgrade to high for complex tasks with the `/model` command. + +You can also use older models by using API-based auth and launching codex with the `--model` flag. + +### Why does `o3` or `o4-mini` not work for me? + +It's possible that your [API account needs to be verified](https://help.openai.com/en/articles/10910291-api-organization-verification) in order to start streaming responses and seeing chain of thought summaries from the API. If you're still running into issues, please let us know! + +### How do I stop Codex from editing my files? + +By default, Codex can modify files in your current working directory (Auto mode). To prevent edits, run `codex` in read-only mode with the CLI flag `--sandbox read-only`. Alternatively, you can change the approval level mid-conversation with `/approvals`. + +### Does it work on Windows? + +Running Codex directly on Windows may work, but is not officially supported. We recommend using [Windows Subsystem for Linux (WSL2)](https://learn.microsoft.com/en-us/windows/wsl/install). \ No newline at end of file diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000000..ba99743133 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,86 @@ +## Getting started + +### CLI usage + +| Command | Purpose | Example | +| ------------------ | ---------------------------------- | ------------------------------- | +| `codex` | Interactive TUI | `codex` | +| `codex "..."` | Initial prompt for interactive TUI | `codex "fix lint errors"` | +| `codex exec "..."` | Non-interactive "automation mode" | `codex exec "explain utils.ts"` | + +Key flags: `--model/-m`, `--ask-for-approval/-a`. + +### Running with a prompt as input + +You can also run Codex CLI with a prompt as input: + +```shell +codex "explain this codebase to me" +``` + +```shell +codex --full-auto "create the fanciest todo-list app" +``` + +That's it - Codex will scaffold a file, run it inside a sandbox, install any +missing dependencies, and show you the live result. Approve the changes and +they'll be committed to your working directory. + +### Example prompts + +Below are a few bite-size examples you can copy-paste. Replace the text in quotes with your own task. See the [prompting guide](https://github.com/openai/codex/blob/main/codex-cli/examples/prompting_guide.md) for more tips and usage patterns. + +| ✨ | What you type | What happens | +| --- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | +| 1 | `codex "Refactor the Dashboard component to React Hooks"` | Codex rewrites the class component, runs `npm test`, and shows the diff. | +| 2 | `codex "Generate SQL migrations for adding a users table"` | Infers your ORM, creates migration files, and runs them in a sandboxed DB. | +| 3 | `codex "Write unit tests for utils/date.ts"` | Generates tests, executes them, and iterates until they pass. | +| 4 | `codex "Bulk-rename *.jpeg -> *.jpg with git mv"` | Safely renames files and updates imports/usages. | +| 5 | `codex "Explain what this regex does: ^(?=.*[A-Z]).{8,}$"` | Outputs a step-by-step human explanation. | +| 6 | `codex "Carefully review this repo, and propose 3 high impact well-scoped PRs"` | Suggests impactful PRs in the current codebase. | +| 7 | `codex "Look for vulnerabilities and create a security review report"` | Finds and explains security bugs. | + +### Memory with AGENTS.md + +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 +2. `AGENTS.md` at repo root - shared project notes +3. `AGENTS.md` in the current working directory - sub-folder/feature specifics + +For more information on how to use AGENTS.md, see the [official AGENTS.md documentation](./agents.md). + +### Tips & shortcuts + +#### Use `@` for file search + +Typing `@` triggers a fuzzy-filename search over the workspace root. Use up/down to select among the results and Tab or Enter to replace the `@` with the selected path. You can use Esc to cancel the search. + +#### Image input + +Paste images directly into the composer (Ctrl+V / Cmd+V) to attach them to your prompt. You can also attach files via the CLI using `-i/--image` (comma‑separated): + +```bash +codex -i screenshot.png "Explain this error" +codex --image img1.png,img2.jpg "Summarize these diagrams" +``` + +#### Esc–Esc to edit a previous message + +When the chat composer is empty, press Esc to prime “backtrack” mode. Press Esc again to open a transcript preview highlighting the last user message; press Esc repeatedly to step to older user messages. Press Enter to confirm and Codex will fork the conversation from that point, trim the visible transcript accordingly, and pre‑fill the composer with the selected user message so you can edit and resubmit it. + +In the transcript preview, the footer shows an `Esc edit prev` hint while editing is active. + +#### Shell completions + +Generate shell completion scripts via: + +```shell +codex completion bash +codex completion zsh +codex completion fish +``` + +#### `--cd`/`-C` flag + +Sometimes it is not convenient to `cd` to the directory you want Codex to use as the "working root" before running Codex. Fortunately, `codex` supports a `--cd` option so you can specify whatever folder you want. You can confirm that Codex is honoring `--cd` by double-checking the **workdir** it reports in the TUI at the start of a new session. diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000000..af3c6a276c --- /dev/null +++ b/docs/install.md @@ -0,0 +1,40 @@ +## Install & build + +### System requirements + +| Requirement | Details | +| --------------------------- | --------------------------------------------------------------- | +| Operating systems | macOS 12+, Ubuntu 20.04+/Debian 10+, or Windows 11 **via WSL2** | +| Git (optional, recommended) | 2.23+ for built-in PR helpers | +| RAM | 4-GB minimum (8-GB recommended) | + +### DotSlash + +The GitHub Release also contains a [DotSlash](https://dotslash-cli.com/) file for the Codex CLI named `codex`. Using a DotSlash file makes it possible to make a lightweight commit to source control to ensure all contributors use the same version of an executable, regardless of what platform they use for development. + +### Build from source + +```bash +# Clone the repository and navigate to the root of the Cargo workspace. +git clone https://github.com/openai/codex.git +cd codex/codex-rs + +# Install the Rust toolchain, if necessary. +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +source "$HOME/.cargo/env" +rustup component add rustfmt +rustup component add clippy + +# Build Codex. +cargo build + +# Launch the TUI with a sample prompt. +cargo run --bin codex -- "explain this codebase to me" + +# After making changes, ensure the code is clean. +cargo fmt -- --config imports_granularity=Item +cargo clippy --tests + +# Run the tests. +cargo test +``` \ No newline at end of file diff --git a/docs/license.md b/docs/license.md new file mode 100644 index 0000000000..8bd6d626c2 --- /dev/null +++ b/docs/license.md @@ -0,0 +1,3 @@ +## License + +This repository is licensed under the [Apache-2.0 License](../LICENSE). \ No newline at end of file diff --git a/docs/open-source-fund.md b/docs/open-source-fund.md new file mode 100644 index 0000000000..4b73477a3a --- /dev/null +++ b/docs/open-source-fund.md @@ -0,0 +1,8 @@ +## Codex open source fund + +We're excited to launch a **$1 million initiative** supporting open source projects that use Codex CLI and other OpenAI models. + +- Grants are awarded up to **$25,000** API credits. +- Applications are reviewed **on a rolling basis**. + +**Interested? [Apply here](https://openai.com/form/codex-open-source-fund/).** \ No newline at end of file diff --git a/docs/platform-sandboxing.md b/docs/platform-sandboxing.md new file mode 100644 index 0000000000..36a7802356 --- /dev/null +++ b/docs/platform-sandboxing.md @@ -0,0 +1,8 @@ +### Platform sandboxing details + +The mechanism Codex uses to implement the sandbox policy depends on your OS: + +- **macOS 12+** uses **Apple Seatbelt** and runs commands using `sandbox-exec` with a profile (`-p`) that corresponds to the `--sandbox` that was specified. +- **Linux** uses a combination of Landlock/seccomp APIs to enforce the `sandbox` configuration. + +Note that when running Linux in a containerized environment such as Docker, sandboxing may not work if the host/container configuration does not support the necessary Landlock/seccomp APIs. In such cases, we recommend configuring your Docker container so that it provides the sandbox guarantees you are looking for and then running `codex` with `--sandbox danger-full-access` (or, more simply, the `--dangerously-bypass-approvals-and-sandbox` flag) within your container. \ No newline at end of file diff --git a/docs/sandbox.md b/docs/sandbox.md new file mode 100644 index 0000000000..dea70f2f81 --- /dev/null +++ b/docs/sandbox.md @@ -0,0 +1,85 @@ +## Sandbox & approvals + +### Approval modes + +We've chosen a powerful default for how Codex works on your computer: `Auto`. In this approval mode, Codex can read files, make edits, and run commands in the working directory automatically. However, Codex will need your approval to work outside the working directory or access network. + +When you just want to chat, or if you want to plan before diving in, you can switch to `Read Only` mode with the `/approvals` command. + +If you need Codex to read files, make edits, and run commands with network access, without approval, you can use `Full Access`. Exercise caution before doing so. + +#### Defaults and recommendations + +- Codex runs in a sandbox by default with strong guardrails: it prevents editing files outside the workspace and blocks network access unless enabled. +- On launch, Codex detects whether the folder is version-controlled and recommends: + - Version-controlled folders: `Auto` (workspace write + on-request approvals) + - Non-version-controlled folders: `Read Only` +- The workspace includes the current directory and temporary directories like `/tmp`. Use the `/status` command to see which directories are in the workspace. +- You can set these explicitly: + - `codex --sandbox workspace-write --ask-for-approval on-request` + - `codex --sandbox read-only --ask-for-approval on-request` + +### Can I run without ANY approvals? + +Yes, you can disable all approval prompts with `--ask-for-approval never`. This option works with all `--sandbox` modes, so you still have full control over Codex's level of autonomy. It will make its best attempt with whatever contrainsts you provide. + +### Common sandbox + approvals combinations + +| Intent | Flags | Effect | +| --------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| Safe read-only browsing | `--sandbox read-only --ask-for-approval on-request` | Codex can read files and answer questions. Codex requires approval to make edits, run commands, or access network. | +| Read-only non-interactive (CI) | `--sandbox read-only --ask-for-approval never` | Reads only; never escalates | +| Let it edit the repo, ask if risky | `--sandbox workspace-write --ask-for-approval on-request` | Codex can read files, make edits, and run commands in the workspace. Codex requires approval for actions outside the workspace or for network access. | +| Auto (preset) | `--full-auto` (equivalent to `--sandbox workspace-write` + `--ask-for-approval on-failure`) | Codex can read files, make edits, and run commands in the workspace. Codex requires approval when a sandboxed command fails or needs escalation. | +| YOLO (not recommended) | `--dangerously-bypass-approvals-and-sandbox` (alias: `--yolo`) | No sandbox; no prompts | + +> Note: In `workspace-write`, network is disabled by default unless enabled in config (`[sandbox_workspace_write].network_access = true`). + +#### Fine-tuning in `config.toml` + +```toml +# approval mode +approval_policy = "untrusted" +sandbox_mode = "read-only" + +# full-auto mode +approval_policy = "on-request" +sandbox_mode = "workspace-write" + +# Optional: allow network in workspace-write mode +[sandbox_workspace_write] +network_access = true +``` + +You can also save presets as **profiles**: + +```toml +[profiles.full_auto] +approval_policy = "on-request" +sandbox_mode = "workspace-write" + +[profiles.readonly_quiet] +approval_policy = "never" +sandbox_mode = "read-only" +``` + +### Experimenting with the Codex Sandbox + +To test to see what happens when a command is run under the sandbox provided by Codex, we provide the following subcommands in Codex CLI: + +``` +# macOS +codex debug seatbelt [--full-auto] [COMMAND]... + +# Linux +codex debug landlock [--full-auto] [COMMAND]... +``` + +### Platform sandboxing details + +The mechanism Codex uses to implement the sandbox policy depends on your OS: + +- **macOS 12+** uses **Apple Seatbelt** and runs commands using `sandbox-exec` with a profile (`-p`) that corresponds to the `--sandbox` that was specified. +- **Linux** uses a combination of Landlock/seccomp APIs to enforce the `sandbox` configuration. + +Note that when running Linux in a containerized environment such as Docker, sandboxing may not work if the host/container configuration does not support the necessary Landlock/seccomp APIs. In such cases, we recommend configuring your Docker container so that it provides the sandbox guarantees you are looking for and then running `codex` with `--sandbox danger-full-access` (or, more simply, the `--dangerously-bypass-approvals-and-sandbox` flag) within your container. \ No newline at end of file diff --git a/docs/zdr.md b/docs/zdr.md new file mode 100644 index 0000000000..92e78a34b6 --- /dev/null +++ b/docs/zdr.md @@ -0,0 +1,15 @@ +## Zero data retention (ZDR) usage + +Codex CLI **does** support OpenAI organizations with [Zero Data Retention (ZDR)](https://platform.openai.com/docs/guides/your-data#zero-data-retention) enabled. If your OpenAI organization has Zero Data Retention enabled and you still encounter errors such as: + +``` +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: + +```toml +disable_response_storage = true +``` + +See [the configuration documentation on `disable_response_storage`](./config.md#disable_response_storage) for details. \ No newline at end of file diff --git a/scripts/readme_toc.py b/scripts/readme_toc.py index fb1ac066a7..b6ab0a6582 100755 --- a/scripts/readme_toc.py +++ b/scripts/readme_toc.py @@ -79,11 +79,11 @@ def check_or_fix(readme_path: Path, fix: bool) -> int: begin_idx = next(i for i, l in enumerate(lines) if l.strip() == BEGIN_TOC) end_idx = next(i for i, l in enumerate(lines) if l.strip() == END_TOC) except StopIteration: + # No ToC markers found; treat as a no-op so repos without a ToC don't fail CI print( - f"Error: Could not locate '{BEGIN_TOC}' or '{END_TOC}' in {readme_path}.", - file=sys.stderr, + f"Note: Skipping ToC check; no markers found in {readme_path}.", ) - return 1 + return 0 # extract current ToC list items current_block = lines[begin_idx + 1 : end_idx] current = [l for l in current_block if l.lstrip().startswith("- [")] From 6e4c9d5243ab77a2c473d75617ca21173fa1cdb9 Mon Sep 17 00:00:00 2001 From: Reuben Narad <139025392+ReubenNarad@users.noreply.github.com> Date: Wed, 27 Aug 2025 11:37:41 -0700 Subject: [PATCH 0371/1309] Added back codex-rs/config.md to link to new location (#2778) Quick fix: point old config.md to new location --- codex-rs/config.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 codex-rs/config.md diff --git a/codex-rs/config.md b/codex-rs/config.md new file mode 100644 index 0000000000..af51df4197 --- /dev/null +++ b/codex-rs/config.md @@ -0,0 +1,6 @@ +# Configuration docs moved + +This file has moved. Please see the latest configuration documentation here: + +- Full config docs: [docs/config.md](../docs/config.md) +- MCP servers section: [docs/config.md#mcp_servers](../docs/config.md#mcp_servers) \ No newline at end of file From 903178eeeb0d9e98a448401f56512c74860644bf Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Wed, 27 Aug 2025 11:45:40 -0700 Subject: [PATCH 0372/1309] Point the CHANGELOG to the releases page (#2780) The typescript changelog is misleading and unhelpful --- CHANGELOG.md | 212 +-------------------------------------------------- 1 file changed, 1 insertion(+), 211 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 899e8a8e22..ed8bb45518 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,211 +1 @@ -# Changelog - -You can install any of these versions: `npm install -g codex@version` - -## `0.1.2505172129` - -### 🪲 Bug Fixes - -- Add node version check (#1007) -- Persist token after refresh (#1006) - -## `0.1.2505171619` - -- `codex --login` + `codex --free` (#998) - -## `0.1.2505161800` - -- Sign in with chatgpt credits (#974) -- Add support for OpenAI tool type, local_shell (#961) - -## `0.1.2505161243` - -- Sign in with chatgpt (#963) -- Session history viewer (#912) -- Apply patch issue when using different cwd (#942) -- Diff command for filenames with special characters (#954) - -## `0.1.2505160811` - -- `codex-mini-latest` (#951) - -## `0.1.2505140839` - -### 🪲 Bug Fixes - -- Gpt-4.1 apply_patch handling (#930) -- Add support for fileOpener in config.json (#911) -- Patch in #366 and #367 for marked-terminal (#916) -- Remember to set lastIndex = 0 on shared RegExp (#918) -- Always load version from package.json at runtime (#909) -- Tweak the label for citations for better rendering (#919) -- Tighten up some logic around session timestamps and ids (#922) -- Change EventMsg enum so every variant takes a single struct (#925) -- Reasoning default to medium, show workdir when supplied (#931) -- Test_dev_null_write() was not using echo as intended (#923) - -## `0.1.2504301751` - -### 🚀 Features - -- User config api key (#569) -- `@mention` files in codex (#701) -- Add `--reasoning` CLI flag (#314) -- Lower default retry wait time and increase number of tries (#720) -- Add common package registries domains to allowed-domains list (#414) - -### 🪲 Bug Fixes - -- Insufficient quota message (#758) -- Input keyboard shortcut opt+delete (#685) -- `/diff` should include untracked files (#686) -- Only allow running without sandbox if explicitly marked in safe container (#699) -- Tighten up check for /usr/bin/sandbox-exec (#710) -- Check if sandbox-exec is available (#696) -- Duplicate messages in quiet mode (#680) - -## `0.1.2504251709` - -### 🚀 Features - -- Add openai model info configuration (#551) -- Added provider to run quiet mode function (#571) -- Create parent directories when creating new files (#552) -- Print bug report URL in terminal instead of opening browser (#510) (#528) -- Add support for custom provider configuration in the user config (#537) -- Add support for OpenAI-Organization and OpenAI-Project headers (#626) -- Add specific instructions for creating API keys in error msg (#581) -- Enhance toCodePoints to prevent potential unicode 14 errors (#615) -- More native keyboard navigation in multiline editor (#655) -- Display error on selection of invalid model (#594) - -### 🪲 Bug Fixes - -- Model selection (#643) -- Nits in apply patch (#640) -- Input keyboard shortcuts (#676) -- `apply_patch` unicode characters (#625) -- Don't clear turn input before retries (#611) -- More loosely match context for apply_patch (#610) -- Update bug report template - there is no --revision flag (#614) -- Remove outdated copy of text input and external editor feature (#670) -- Remove unreachable "disableResponseStorage" logic flow introduced in #543 (#573) -- Non-openai mode - fix for gemini content: null, fix 429 to throw before stream (#563) -- Only allow going up in history when not already in history if input is empty (#654) -- Do not grant "node" user sudo access when using run_in_container.sh (#627) -- Update scripts/build_container.sh to use pnpm instead of npm (#631) -- Update lint-staged config to use pnpm --filter (#582) -- Non-openai mode - don't default temp and top_p (#572) -- Fix error catching when checking for updates (#597) -- Close stdin when running an exec tool call (#636) - -## `0.1.2504221401` - -### 🚀 Features - -- Show actionable errors when api keys are missing (#523) -- Add CLI `--version` flag (#492) - -### 🪲 Bug Fixes - -- Agent loop for ZDR (`disableResponseStorage`) (#543) -- Fix relative `workdir` check for `apply_patch` (#556) -- Minimal mid-stream #429 retry loop using existing back-off (#506) -- Inconsistent usage of base URL and API key (#507) -- Remove requirement for api key for ollama (#546) -- Support `[provider]_BASE_URL` (#542) - -## `0.1.2504220136` - -### 🚀 Features - -- Add support for ZDR orgs (#481) -- Include fractional portion of chunk that exceeds stdout/stderr limit (#497) - -## `0.1.2504211509` - -### 🚀 Features - -- Support multiple providers via Responses-Completion transformation (#247) -- Add user-defined safe commands configuration and approval logic #380 (#386) -- Allow switching approval modes when prompted to approve an edit/command (#400) -- Add support for `/diff` command autocomplete in TerminalChatInput (#431) -- Auto-open model selector if user selects deprecated model (#427) -- Read approvalMode from config file (#298) -- `/diff` command to view git diff (#426) -- Tab completions for file paths (#279) -- Add /command autocomplete (#317) -- Allow multi-line input (#438) - -### 🪲 Bug Fixes - -- `full-auto` support in quiet mode (#374) -- Enable shell option for child process execution (#391) -- Configure husky and lint-staged for pnpm monorepo (#384) -- Command pipe execution by improving shell detection (#437) -- Name of the file not matching the name of the component (#354) -- Allow proper exit from new Switch approval mode dialog (#453) -- Ensure /clear resets context and exclude system messages from approximateTokenUsed count (#443) -- `/clear` now clears terminal screen and resets context left indicator (#425) -- Correct fish completion function name in CLI script (#485) -- Auto-open model-selector when model is not found (#448) -- Remove unnecessary isLoggingEnabled() checks (#420) -- Improve test reliability for `raw-exec` (#434) -- Unintended tear down of agent loop (#483) -- Remove extraneous type casts (#462) - -## `0.1.2504181820` - -### 🚀 Features - -- Add `/bug` report command (#312) -- Notify when a newer version is available (#333) - -### 🪲 Bug Fixes - -- Update context left display logic in TerminalChatInput component (#307) -- Improper spawn of sh on Windows Powershell (#318) -- `/bug` report command, thinking indicator (#381) -- Include pnpm lock file (#377) - -## `0.1.2504172351` - -### 🚀 Features - -- Add Nix flake for reproducible development environments (#225) - -### 🪲 Bug Fixes - -- Handle invalid commands (#304) -- Raw-exec-process-group.test improve reliability and error handling (#280) -- Canonicalize the writeable paths used in seatbelt policy (#275) - -## `0.1.2504172304` - -### 🚀 Features - -- Add shell completion subcommand (#138) -- Add command history persistence (#152) -- Shell command explanation option (#173) -- Support bun fallback runtime for codex CLI (#282) -- Add notifications for MacOS using Applescript (#160) -- Enhance image path detection in input processing (#189) -- `--config`/`-c` flag to open global instructions in nvim (#158) -- Update position of cursor when navigating input history with arrow keys to the end of the text (#255) - -### 🪲 Bug Fixes - -- Correct word deletion logic for trailing spaces (Ctrl+Backspace) (#131) -- Improve Windows compatibility for CLI commands and sandbox (#261) -- Correct typos in thinking texts (transcendent & parroting) (#108) -- Add empty vite config file to prevent resolving to parent (#273) -- Update regex to better match the retry error messages (#266) -- Add missing "as" in prompt prefix in agent loop (#186) -- Allow continuing after interrupting assistant (#178) -- Standardize filename to kebab-case 🐍➡️🥙 (#302) -- Small update to bug report template (#288) -- Duplicated message on model change (#276) -- Typos in prompts and comments (#195) -- Check workdir before spawn (#221) - - +The changelog can be found on the [releases page](https://github.com/openai/codex/releases) From 488a40211abd382cf5b7b9969886b02076189a88 Mon Sep 17 00:00:00 2001 From: Jeremy Rose <172423086+nornagon-openai@users.noreply.github.com> Date: Wed, 27 Aug 2025 13:55:59 -0700 Subject: [PATCH 0373/1309] fix (most) doubled lines and hanging list markers (#2789) This was mostly written by codex under heavy guidance via test cases drawn from logged session data and fuzzing. It also uncovered some bugs in tui_markdown, which will in some cases split a list marker from the list item content. We're not addressing those bugs for now. --- codex-rs/tui/src/markdown.rs | 144 ++++++++++ codex-rs/tui/src/markdown_stream.rs | 317 +++++++++++++++++++++++ codex-rs/tui/src/streaming/controller.rs | 186 +++++++++++++ 3 files changed, 647 insertions(+) diff --git a/codex-rs/tui/src/markdown.rs b/codex-rs/tui/src/markdown.rs index 8adc8f3b96..fcb9e774a0 100644 --- a/codex-rs/tui/src/markdown.rs +++ b/codex-rs/tui/src/markdown.rs @@ -435,4 +435,148 @@ mod tests { "Hi! How can I help with codex-rs today? Want me to explore the repo, run tests, or work on a specific change?" ); } + + #[test] + fn tui_markdown_splits_ordered_marker_and_text() { + // With marker and content on the same line, tui_markdown keeps it as one line + // even in the surrounding section context. + let rendered = tui_markdown::from_str("Loose vs. tight list items:\n1. Tight item\n"); + let lines: Vec = rendered + .lines + .iter() + .map(|l| { + l.spans + .iter() + .map(|s| s.content.clone()) + .collect::() + }) + .collect(); + assert!( + lines.iter().any(|w| w == "1. Tight item"), + "expected single line '1. Tight item' in context: {lines:?}" + ); + } + + #[test] + fn append_markdown_matches_tui_markdown_for_ordered_item() { + use codex_core::config_types::UriBasedFileOpener; + use std::path::Path; + let cwd = Path::new("/"); + let mut out = Vec::new(); + append_markdown_with_opener_and_cwd( + "1. Tight item\n", + &mut out, + UriBasedFileOpener::None, + cwd, + ); + let lines: Vec = out + .iter() + .map(|l| { + l.spans + .iter() + .map(|s| s.content.clone()) + .collect::() + }) + .collect(); + assert_eq!(lines, vec!["1. Tight item".to_string()]); + } + + #[test] + fn tui_markdown_shape_for_loose_tight_section() { + // Use the exact source from the session deltas used in tests. + let source = r#" +Loose vs. tight list items: +1. Tight item +2. Another tight item + +3. + Loose item +"#; + + let rendered = tui_markdown::from_str(source); + let lines: Vec = rendered + .lines + .iter() + .map(|l| { + l.spans + .iter() + .map(|s| s.content.clone()) + .collect::() + }) + .collect(); + // Join into a single string and assert the exact shape we observe + // from tui_markdown in this larger context (marker and content split). + let joined = { + let mut s = String::new(); + for (i, l) in lines.iter().enumerate() { + s.push_str(l); + if i + 1 < lines.len() { + s.push('\n'); + } + } + s + }; + let expected = r#"Loose vs. tight list items: + +1. +Tight item +2. +Another tight item +3. +Loose item"#; + assert_eq!( + joined, expected, + "unexpected tui_markdown shape: {joined:?}" + ); + } + + #[test] + fn split_text_and_fences_keeps_ordered_list_line_as_text() { + // No fences here; expect a single Text segment containing the full input. + let src = "Loose vs. tight list items:\n1. Tight item\n"; + let segs = super::split_text_and_fences(src); + assert_eq!( + segs.len(), + 1, + "expected single text segment, got {}", + segs.len() + ); + match &segs[0] { + super::Segment::Text(s) => assert_eq!(s, src), + _ => panic!("expected Text segment for non-fence input"), + } + } + + #[test] + fn append_markdown_keeps_ordered_list_line_unsplit_in_context() { + use codex_core::config_types::UriBasedFileOpener; + use std::path::Path; + let src = "Loose vs. tight list items:\n1. Tight item\n"; + let cwd = Path::new("/"); + let mut out = Vec::new(); + append_markdown_with_opener_and_cwd(src, &mut out, UriBasedFileOpener::None, cwd); + + let lines: Vec = out + .iter() + .map(|l| { + l.spans + .iter() + .map(|s| s.content.clone()) + .collect::() + }) + .collect(); + + // Expect to find the ordered list line rendered as a single line, + // not split into a marker-only line followed by the text. + assert!( + lines.iter().any(|s| s == "1. Tight item"), + "expected '1. Tight item' rendered as a single line; got: {lines:?}" + ); + assert!( + !lines + .windows(2) + .any(|w| w[0].trim_end() == "1." && w[1] == "Tight item"), + "did not expect a split into ['1.', 'Tight item']; got: {lines:?}" + ); + } } diff --git a/codex-rs/tui/src/markdown_stream.rs b/codex-rs/tui/src/markdown_stream.rs index af928cf8e1..5dc84cb11d 100644 --- a/codex-rs/tui/src/markdown_stream.rs +++ b/codex-rs/tui/src/markdown_stream.rs @@ -65,6 +65,21 @@ impl MarkdownStreamCollector { { complete_line_count -= 1; } + // Heuristic: if the buffer ends with a double newline and the last non-blank + // rendered line looks like a list bullet with inline content (e.g., "- item"), + // defer committing that line. Subsequent context (e.g., another list item) + // can cause the renderer to split the bullet marker and text into separate + // logical lines ("- " then "item"), which would otherwise duplicate content. + if self.buffer.ends_with("\n\n") && complete_line_count > 0 { + let last = &rendered[complete_line_count - 1]; + let mut text = String::new(); + for s in &last.spans { + text.push_str(&s.content); + } + if text.starts_with("- ") && text.trim() != "-" { + complete_line_count = complete_line_count.saturating_sub(1); + } + } if !self.buffer.ends_with('\n') { complete_line_count = complete_line_count.saturating_sub(1); // If we're inside an unclosed fenced code block, also drop the @@ -72,6 +87,38 @@ impl MarkdownStreamCollector { if is_inside_unclosed_fence(&source) { complete_line_count = complete_line_count.saturating_sub(1); } + // If the next (incomplete) line appears to begin a list item, + // also defer the previous completed line because the renderer may + // retroactively treat it as part of the list (e.g., ordered list item 1). + if let Some(last_nl) = source.rfind('\n') { + let tail = &source[last_nl + 1..]; + if starts_with_list_marker(tail) { + complete_line_count = complete_line_count.saturating_sub(1); + } + } + } + + // Conservatively withhold trailing list-like lines (unordered or ordered) + // because streaming mid-item can cause the renderer to later split or + // restructure them (e.g., duplicating content or separating the marker). + // Only defers lines at the end of the out slice so previously committed + // lines remain stable. + if complete_line_count > self.committed_line_count { + let mut safe_count = complete_line_count; + while safe_count > self.committed_line_count { + let l = &rendered[safe_count - 1]; + let mut text = String::new(); + for s in &l.spans { + text.push_str(&s.content); + } + let listish = is_potentially_volatile_list_line(&text); + if listish { + safe_count -= 1; + continue; + } + break; + } + complete_line_count = safe_count; } if self.committed_line_count >= complete_line_count { @@ -86,6 +133,20 @@ impl MarkdownStreamCollector { return Vec::new(); } + // Additional conservative hold-back: if exactly one short, plain word + // line would be emitted, defer it. This avoids committing a lone word + // that might become the first ordered-list item once the next delta + // arrives (e.g., next line starts with "2 " or "2. "). + if out_slice.len() == 1 { + let mut s = String::new(); + for sp in &out_slice[0].spans { + s.push_str(&sp.content); + } + if is_short_plain_word(&s) { + return Vec::new(); + } + } + let out = out_slice.to_vec(); self.committed_line_count = complete_line_count; out @@ -118,6 +179,75 @@ impl MarkdownStreamCollector { } } +#[inline] +fn is_potentially_volatile_list_line(text: &str) -> bool { + let t = text.trim_end(); + if t == "-" || t == "*" || t == "- " || t == "* " { + return true; + } + if t.starts_with("- ") || t.starts_with("* ") { + return true; + } + // ordered list like "1. " or "23. " + let mut it = t.chars().peekable(); + let mut saw_digit = false; + while let Some(&ch) = it.peek() { + if ch.is_ascii_digit() { + saw_digit = true; + it.next(); + continue; + } + break; + } + if saw_digit && it.peek() == Some(&'.') { + // consume '.' + it.next(); + if it.peek() == Some(&' ') { + return true; + } + } + false +} + +#[inline] +fn starts_with_list_marker(text: &str) -> bool { + let t = text.trim_start(); + if t.starts_with("- ") || t.starts_with("* ") || t.starts_with("-\t") || t.starts_with("*\t") { + return true; + } + // ordered list marker like "1 ", "1. ", "23 ", "23. " + let mut it = t.chars().peekable(); + let mut saw_digit = false; + while let Some(&ch) = it.peek() { + if ch.is_ascii_digit() { + saw_digit = true; + it.next(); + } else { + break; + } + } + if !saw_digit { + return false; + } + match it.peek() { + Some('.') => { + it.next(); + matches!(it.peek(), Some(' ')) + } + Some(' ') => true, + _ => false, + } +} + +#[inline] +fn is_short_plain_word(s: &str) -> bool { + let t = s.trim(); + if t.is_empty() || t.len() > 5 { + return false; + } + t.chars().all(|c| c.is_alphanumeric()) +} + /// fence helpers are provided by `crate::render::markdown_utils` #[cfg(test)] fn unwrap_markdown_language_fence_if_enabled(s: String) -> String { @@ -530,4 +660,191 @@ mod tests { "heading should not merge with paragraph: {texts:?}" ); } + + #[test] + fn loose_list_with_split_dashes_matches_full_render() { + let cfg = test_config(); + // Minimized failing sequence discovered by the helper: two chunks + // that still reproduce the mismatch. + let deltas = vec!["- item.\n\n", "-"]; + + let streamed = simulate_stream_markdown_for_tests(&deltas, true, &cfg); + let streamed_strs = lines_to_plain_strings(&streamed); + + let full: String = deltas.iter().copied().collect(); + let mut rendered_all: Vec> = Vec::new(); + crate::markdown::append_markdown(&full, &mut rendered_all, &cfg); + let rendered_all_strs = lines_to_plain_strings(&rendered_all); + + assert_eq!( + streamed_strs, rendered_all_strs, + "streamed output should match full render without dangling '-' lines" + ); + } + + #[test] + fn loose_vs_tight_list_items_streaming_matches_full() { + let cfg = test_config(); + // Deltas extracted from the session log around 2025-08-27T00:33:18.216Z + let deltas = vec![ + "\n\n", + "Loose", + " vs", + ".", + " tight", + " list", + " items", + ":\n", + "1", + ".", + " Tight", + " item", + "\n", + "2", + ".", + " Another", + " tight", + " item", + "\n\n", + "1", + ".", + " Loose", + " item", + " with", + " its", + " own", + " paragraph", + ".\n\n", + " ", + " This", + " paragraph", + " belongs", + " to", + " the", + " same", + " list", + " item", + ".\n\n", + "2", + ".", + " Second", + " loose", + " item", + " with", + " a", + " nested", + " list", + " after", + " a", + " blank", + " line", + ".\n\n", + " ", + " -", + " Nested", + " bullet", + " under", + " a", + " loose", + " item", + "\n", + " ", + " -", + " Another", + " nested", + " bullet", + "\n\n", + ]; + + let streamed = simulate_stream_markdown_for_tests(&deltas, true, &cfg); + let streamed_strs = lines_to_plain_strings(&streamed); + + // Compute a full render for diagnostics only. + let full: String = deltas.iter().copied().collect(); + let mut rendered_all: Vec> = Vec::new(); + crate::markdown::append_markdown(&full, &mut rendered_all, &cfg); + + // Also assert exact expected plain strings for clarity. + let expected = vec![ + "Loose vs. tight list items:".to_string(), + "".to_string(), + "1. ".to_string(), + "Tight item".to_string(), + "2. ".to_string(), + "Another tight item".to_string(), + "3. ".to_string(), + "Loose item with its own paragraph.".to_string(), + "".to_string(), + "This paragraph belongs to the same list item.".to_string(), + "4. ".to_string(), + "Second loose item with a nested list after a blank line.".to_string(), + " - Nested bullet under a loose item".to_string(), + " - Another nested bullet".to_string(), + ]; + assert_eq!( + streamed_strs, expected, + "expected exact rendered lines for loose/tight section" + ); + } + + // Targeted tests derived from fuzz findings. Each asserts streamed == full render. + + #[test] + fn fuzz_class_bare_dash_then_task_item() { + let cfg = test_config(); + // Case similar to: ["two\n", "- \n* [x] done "] + let deltas = vec!["two\n", "- \n* [x] done \n"]; + let streamed = simulate_stream_markdown_for_tests(&deltas, true, &cfg); + let streamed_strs = lines_to_plain_strings(&streamed); + let full: String = deltas.iter().copied().collect(); + let mut rendered: Vec> = Vec::new(); + crate::markdown::append_markdown(&full, &mut rendered, &cfg); + let rendered_strs = lines_to_plain_strings(&rendered); + assert_eq!(streamed_strs, rendered_strs); + } + + #[test] + fn fuzz_class_bullet_duplication_variant_1() { + let cfg = test_config(); + // Case similar to: ["aph.\n- let one\n- bull", "et two\n\n second paragraph "] + let deltas = vec!["aph.\n- let one\n- bull", "et two\n\n second paragraph \n"]; + let streamed = simulate_stream_markdown_for_tests(&deltas, true, &cfg); + let streamed_strs = lines_to_plain_strings(&streamed); + let full: String = deltas.iter().copied().collect(); + let mut rendered: Vec> = Vec::new(); + crate::markdown::append_markdown(&full, &mut rendered, &cfg); + let rendered_strs = lines_to_plain_strings(&rendered); + assert_eq!(streamed_strs, rendered_strs); + } + + #[test] + fn fuzz_class_bullet_duplication_variant_2() { + let cfg = test_config(); + // Case similar to: ["- e\n c", "e\n- bullet two\n\n second paragraph in bullet two\n"] + let deltas = vec![ + "- e\n c", + "e\n- bullet two\n\n second paragraph in bullet two\n", + ]; + let streamed = simulate_stream_markdown_for_tests(&deltas, true, &cfg); + let streamed_strs = lines_to_plain_strings(&streamed); + let full: String = deltas.iter().copied().collect(); + let mut rendered: Vec> = Vec::new(); + crate::markdown::append_markdown(&full, &mut rendered, &cfg); + let rendered_strs = lines_to_plain_strings(&rendered); + assert_eq!(streamed_strs, rendered_strs); + } + + #[test] + fn fuzz_class_ordered_list_split_weirdness() { + let cfg = test_config(); + // Case similar to: ["one\n2", " two\n- \n* [x] d"] + let deltas = vec!["one\n2", " two\n- \n* [x] d\n"]; + let streamed = simulate_stream_markdown_for_tests(&deltas, true, &cfg); + let streamed_strs = lines_to_plain_strings(&streamed); + let full: String = deltas.iter().copied().collect(); + let mut rendered: Vec> = Vec::new(); + crate::markdown::append_markdown(&full, &mut rendered, &cfg); + let rendered_strs = lines_to_plain_strings(&rendered); + assert_eq!(streamed_strs, rendered_strs); + } } diff --git a/codex-rs/tui/src/streaming/controller.rs b/codex-rs/tui/src/streaming/controller.rs index b9afa89392..9c6db452ba 100644 --- a/codex-rs/tui/src/streaming/controller.rs +++ b/codex-rs/tui/src/streaming/controller.rs @@ -214,3 +214,189 @@ impl StreamController { self.finalize(true, sink) } } + +#[cfg(test)] +mod tests { + use super::*; + use codex_core::config::Config; + use codex_core::config::ConfigOverrides; + use std::cell::RefCell; + + fn test_config() -> Config { + let overrides = ConfigOverrides { + cwd: std::env::current_dir().ok(), + ..Default::default() + }; + match Config::load_with_cli_overrides(vec![], overrides) { + Ok(c) => c, + Err(e) => panic!("load test config: {e}"), + } + } + + struct TestSink { + pub lines: RefCell>>>, + } + impl TestSink { + fn new() -> Self { + Self { + lines: RefCell::new(Vec::new()), + } + } + } + impl HistorySink for TestSink { + fn insert_history(&self, lines: Vec>) { + self.lines.borrow_mut().push(lines); + } + fn start_commit_animation(&self) {} + fn stop_commit_animation(&self) {} + } + + fn lines_to_plain_strings(lines: &[ratatui::text::Line<'_>]) -> Vec { + lines + .iter() + .map(|l| { + l.spans + .iter() + .map(|s| s.content.clone()) + .collect::>() + .join("") + }) + .collect() + } + + #[test] + fn controller_loose_vs_tight_with_commit_ticks_matches_full() { + let cfg = test_config(); + let mut ctrl = StreamController::new(cfg.clone()); + let sink = TestSink::new(); + ctrl.begin(&sink); + + // Exact deltas from the session log (section: Loose vs. tight list items) + let deltas = vec![ + "\n\n", + "Loose", + " vs", + ".", + " tight", + " list", + " items", + ":\n", + "1", + ".", + " Tight", + " item", + "\n", + "2", + ".", + " Another", + " tight", + " item", + "\n\n", + "1", + ".", + " Loose", + " item", + " with", + " its", + " own", + " paragraph", + ".\n\n", + " ", + " This", + " paragraph", + " belongs", + " to", + " the", + " same", + " list", + " item", + ".\n\n", + "2", + ".", + " Second", + " loose", + " item", + " with", + " a", + " nested", + " list", + " after", + " a", + " blank", + " line", + ".\n\n", + " ", + " -", + " Nested", + " bullet", + " under", + " a", + " loose", + " item", + "\n", + " ", + " -", + " Another", + " nested", + " bullet", + "\n\n", + ]; + + // Simulate streaming with a commit tick attempt after each delta. + for d in &deltas { + ctrl.push_and_maybe_commit(d, &sink); + let _ = ctrl.on_commit_tick(&sink); + } + // Finalize and flush remaining lines now. + let _ = ctrl.finalize(true, &sink); + + // Flatten sink output and strip the header that the controller inserts (blank + "codex"). + let mut flat: Vec> = Vec::new(); + for batch in sink.lines.borrow().iter() { + for l in batch { + flat.push(l.clone()); + } + } + // Drop leading blank and header line if present. + if !flat.is_empty() && lines_to_plain_strings(&[flat[0].clone()])[0].is_empty() { + flat.remove(0); + } + if !flat.is_empty() { + let s0 = lines_to_plain_strings(&[flat[0].clone()])[0].clone(); + if s0 == "codex" { + flat.remove(0); + } + } + let streamed = lines_to_plain_strings(&flat); + + // Full render of the same source + let source: String = deltas.iter().copied().collect(); + let mut rendered: Vec> = Vec::new(); + crate::markdown::append_markdown(&source, &mut rendered, &cfg); + let rendered_strs = lines_to_plain_strings(&rendered); + + assert_eq!(streamed, rendered_strs); + + // Also assert exact expected plain strings for clarity. + let expected = vec![ + "Loose vs. tight list items:".to_string(), + "".to_string(), + "1. ".to_string(), + "Tight item".to_string(), + "2. ".to_string(), + "Another tight item".to_string(), + "3. ".to_string(), + "Loose item with its own paragraph.".to_string(), + "".to_string(), + "This paragraph belongs to the same list item.".to_string(), + "4. ".to_string(), + "Second loose item with a nested list after a blank line.".to_string(), + " - Nested bullet under a loose item".to_string(), + " - Another nested bullet".to_string(), + ]; + assert_eq!( + streamed, expected, + "expected exact rendered lines for loose/tight section" + ); + } +} From 3e309805ae34308a16f3ee1eb2147d67286c1f26 Mon Sep 17 00:00:00 2001 From: Jeremy Rose <172423086+nornagon-openai@users.noreply.github.com> Date: Wed, 27 Aug 2025 14:17:10 -0700 Subject: [PATCH 0374/1309] fix cursor after suspend (#2690) This was supposed to be fixed by #2569, but I think the actual fix got lost in the refactoring. Intended behavior: pressing ^Z moves the cursor below the viewport before suspending. --- codex-rs/tui/src/tui.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/codex-rs/tui/src/tui.rs b/codex-rs/tui/src/tui.rs index f9d74989e8..47a858c55a 100644 --- a/codex-rs/tui/src/tui.rs +++ b/codex-rs/tui/src/tui.rs @@ -7,6 +7,8 @@ use std::sync::Arc; use std::sync::atomic::AtomicBool; #[cfg(unix)] use std::sync::atomic::AtomicU8; +#[cfg(unix)] +use std::sync::atomic::AtomicU16; use std::sync::atomic::Ordering; use std::time::Duration; use std::time::Instant; @@ -168,6 +170,8 @@ pub struct Tui { alt_saved_viewport: Option, #[cfg(unix)] resume_pending: Arc, // Stores a ResumeAction + #[cfg(unix)] + suspend_cursor_y: Arc, // Bottom line of inline viewport // True when overlay alt-screen UI is active alt_screen_active: Arc, } @@ -274,6 +278,8 @@ impl Tui { alt_saved_viewport: None, #[cfg(unix)] resume_pending: Arc::new(AtomicU8::new(0)), + #[cfg(unix)] + suspend_cursor_y: Arc::new(AtomicU16::new(0)), alt_screen_active: Arc::new(AtomicBool::new(false)), } } @@ -292,6 +298,8 @@ impl Tui { let resume_pending = self.resume_pending.clone(); #[cfg(unix)] let alt_screen_active = self.alt_screen_active.clone(); + #[cfg(unix)] + let suspend_cursor_y = self.suspend_cursor_y.clone(); let event_stream = async_stream::stream! { loop { select! { @@ -340,6 +348,11 @@ impl Tui { } else { resume_pending.store(ResumeAction::RealignInline as u8, Ordering::Relaxed); } + #[cfg(unix)] + { + let y = suspend_cursor_y.load(Ordering::Relaxed); + let _ = execute!(stdout(), MoveTo(0, y)); + } let _ = execute!(stdout(), crossterm::cursor::Show); let _ = Tui::suspend(); yield TuiEvent::Draw; @@ -396,7 +409,7 @@ impl Tui { ))) } ResumeAction::RestoreAlt => { - if let Ok((_x, y)) = crossterm::cursor::position() + if let Ok(ratatui::layout::Position { y, .. }) = self.terminal.get_cursor_position() && let Some(saved) = self.alt_saved_viewport.as_mut() { saved.y = y; @@ -532,6 +545,19 @@ impl Tui { ); self.pending_history_lines.clear(); } + // Update the y position for suspending so Ctrl-Z can place the cursor correctly. + #[cfg(unix)] + { + let inline_area_bottom = if self.alt_screen_active.load(Ordering::Relaxed) { + self.alt_saved_viewport + .map(|r| r.bottom().saturating_sub(1)) + .unwrap_or_else(|| area.bottom().saturating_sub(1)) + } else { + area.bottom().saturating_sub(1) + }; + self.suspend_cursor_y + .store(inline_area_bottom, Ordering::Relaxed); + } terminal.draw(|frame| { draw_fn(frame); })?; From 4e9ad238649c71690cbb0402e110943223c16fcd Mon Sep 17 00:00:00 2001 From: dedrisian-oai Date: Wed, 27 Aug 2025 17:41:23 -0700 Subject: [PATCH 0375/1309] Add "View Image" tool (#2723) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a "View Image" tool so Codex can find and see images by itself: Screenshot 2025-08-26 at 10 40
04 AM --- codex-rs/core/src/codex.rs | 33 ++++++++ codex-rs/core/src/config.rs | 17 ++++ codex-rs/core/src/openai_tools.rs | 83 ++++++++++++++++--- codex-rs/core/tests/suite/prompt_caching.rs | 2 +- codex-rs/exec/src/lib.rs | 1 + .../mcp-server/src/codex_message_processor.rs | 1 + codex-rs/mcp-server/src/codex_tool_config.rs | 1 + codex-rs/tui/src/lib.rs | 1 + 8 files changed, 126 insertions(+), 13 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index cfc94016b0..365969ac02 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -518,6 +518,7 @@ impl Session { include_apply_patch_tool: config.include_apply_patch_tool, include_web_search_request: config.tools_web_search_request, use_streamable_shell_tool: config.use_experimental_streamable_shell_tool, + include_view_image_tool: config.include_view_image_tool, }), user_instructions, base_instructions, @@ -1108,6 +1109,7 @@ async fn submission_loop( include_apply_patch_tool: config.include_apply_patch_tool, include_web_search_request: config.tools_web_search_request, use_streamable_shell_tool: config.use_experimental_streamable_shell_tool, + include_view_image_tool: config.include_view_image_tool, }); let new_turn_context = TurnContext { @@ -1193,6 +1195,7 @@ async fn submission_loop( include_web_search_request: config.tools_web_search_request, use_streamable_shell_tool: config .use_experimental_streamable_shell_tool, + include_view_image_tool: config.include_view_image_tool, }), user_instructions: turn_context.user_instructions.clone(), base_instructions: turn_context.base_instructions.clone(), @@ -2077,6 +2080,36 @@ async fn handle_function_call( ) .await } + "view_image" => { + #[derive(serde::Deserialize)] + struct SeeImageArgs { + path: String, + } + let args = match serde_json::from_str::(&arguments) { + Ok(a) => a, + Err(e) => { + return ResponseInputItem::FunctionCallOutput { + call_id, + output: FunctionCallOutputPayload { + content: format!("failed to parse function arguments: {e}"), + success: Some(false), + }, + }; + } + }; + let abs = turn_context.resolve_path(Some(args.path)); + let output = match sess.inject_input(vec![InputItem::LocalImage { path: abs }]) { + Ok(()) => FunctionCallOutputPayload { + content: "attached local image path".to_string(), + success: Some(true), + }, + Err(_) => FunctionCallOutputPayload { + content: "unable to attach image (no active task)".to_string(), + success: Some(false), + }, + }; + ResponseInputItem::FunctionCallOutput { call_id, output } + } "apply_patch" => { let args = match serde_json::from_str::(&arguments) { Ok(a) => a, diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 98a8fde135..9b8f288cf3 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -178,6 +178,9 @@ pub struct Config { pub preferred_auth_method: AuthMode, pub use_experimental_streamable_shell_tool: bool, + + /// Include the `view_image` tool that lets the agent attach a local image path to context. + pub include_view_image_tool: bool, } impl Config { @@ -497,6 +500,10 @@ pub struct ToolsToml { // Renamed from `web_search_request`; keep alias for backwards compatibility. #[serde(default, alias = "web_search_request")] pub web_search: Option, + + /// Enable the `view_image` tool that lets the agent attach local images. + #[serde(default)] + pub view_image: Option, } impl ConfigToml { @@ -586,6 +593,7 @@ pub struct ConfigOverrides { pub base_instructions: Option, pub include_plan_tool: Option, pub include_apply_patch_tool: Option, + pub include_view_image_tool: Option, pub disable_response_storage: Option, pub show_raw_agent_reasoning: Option, pub tools_web_search_request: Option, @@ -613,6 +621,7 @@ impl Config { base_instructions, include_plan_tool, include_apply_patch_tool, + include_view_image_tool, disable_response_storage, show_raw_agent_reasoning, tools_web_search_request: override_tools_web_search_request, @@ -681,6 +690,10 @@ impl Config { .or(cfg.tools.as_ref().and_then(|t| t.web_search)) .unwrap_or(false); + let include_view_image_tool = include_view_image_tool + .or(cfg.tools.as_ref().and_then(|t| t.view_image)) + .unwrap_or(true); + let model = model .or(config_profile.model) .or(cfg.model) @@ -784,6 +797,7 @@ impl Config { use_experimental_streamable_shell_tool: cfg .experimental_use_exec_command_tool .unwrap_or(false), + include_view_image_tool, }; Ok(config) } @@ -1152,6 +1166,7 @@ disable_response_storage = true responses_originator_header: "codex_cli_rs".to_string(), preferred_auth_method: AuthMode::ChatGPT, use_experimental_streamable_shell_tool: false, + include_view_image_tool: true, }, o3_profile_config ); @@ -1208,6 +1223,7 @@ disable_response_storage = true responses_originator_header: "codex_cli_rs".to_string(), preferred_auth_method: AuthMode::ChatGPT, use_experimental_streamable_shell_tool: false, + include_view_image_tool: true, }; assert_eq!(expected_gpt3_profile_config, gpt3_profile_config); @@ -1279,6 +1295,7 @@ disable_response_storage = true responses_originator_header: "codex_cli_rs".to_string(), preferred_auth_method: AuthMode::ChatGPT, use_experimental_streamable_shell_tool: false, + include_view_image_tool: true, }; assert_eq!(expected_zdr_profile_config, zdr_profile_config); diff --git a/codex-rs/core/src/openai_tools.rs b/codex-rs/core/src/openai_tools.rs index a9fdb4f0e4..f74188162c 100644 --- a/codex-rs/core/src/openai_tools.rs +++ b/codex-rs/core/src/openai_tools.rs @@ -67,6 +67,7 @@ pub(crate) struct ToolsConfig { pub plan_tool: bool, pub apply_patch_tool_type: Option, pub web_search_request: bool, + pub include_view_image_tool: bool, } pub(crate) struct ToolsConfigParams<'a> { @@ -77,6 +78,7 @@ pub(crate) struct ToolsConfigParams<'a> { pub(crate) include_apply_patch_tool: bool, pub(crate) include_web_search_request: bool, pub(crate) use_streamable_shell_tool: bool, + pub(crate) include_view_image_tool: bool, } impl ToolsConfig { @@ -89,6 +91,7 @@ impl ToolsConfig { include_apply_patch_tool, include_web_search_request, use_streamable_shell_tool, + include_view_image_tool, } = params; let mut shell_type = if *use_streamable_shell_tool { ConfigShellToolType::StreamableShell @@ -120,6 +123,7 @@ impl ToolsConfig { plan_tool: *include_plan_tool, apply_patch_tool_type, web_search_request: *include_web_search_request, + include_view_image_tool: *include_view_image_tool, } } } @@ -292,6 +296,30 @@ The shell tool is used to execute shell commands. }, }) } + +fn create_view_image_tool() -> OpenAiTool { + // Support only local filesystem path. + let mut properties = BTreeMap::new(); + properties.insert( + "path".to_string(), + JsonSchema::String { + description: Some("Local filesystem path to an image file".to_string()), + }, + ); + + OpenAiTool::Function(ResponsesApiTool { + name: "view_image".to_string(), + description: + "Attach a local image (by filesystem path) to the conversation context for this turn." + .to_string(), + strict: false, + parameters: JsonSchema::Object { + properties, + required: Some(vec!["path".to_string()]), + additional_properties: Some(false), + }, + }) +} /// TODO(dylan): deprecate once we get rid of json tool #[derive(Serialize, Deserialize)] pub(crate) struct ApplyPatchToolArgs { @@ -541,6 +569,11 @@ pub(crate) fn get_openai_tools( tools.push(OpenAiTool::WebSearch {}); } + // Include the view_image tool so the agent can attach images to context. + if config.include_view_image_tool { + tools.push(create_view_image_tool()); + } + if let Some(mcp_tools) = mcp_tools { // Ensure deterministic ordering to maximize prompt cache hits. // HashMap iteration order is non-deterministic, so sort by fully-qualified tool name. @@ -604,10 +637,14 @@ mod tests { include_apply_patch_tool: false, include_web_search_request: true, use_streamable_shell_tool: false, + include_view_image_tool: true, }); let tools = get_openai_tools(&config, Some(HashMap::new())); - assert_eq_tool_names(&tools, &["local_shell", "update_plan", "web_search"]); + assert_eq_tool_names( + &tools, + &["local_shell", "update_plan", "web_search", "view_image"], + ); } #[test] @@ -621,10 +658,14 @@ mod tests { include_apply_patch_tool: false, include_web_search_request: true, use_streamable_shell_tool: false, + include_view_image_tool: true, }); let tools = get_openai_tools(&config, Some(HashMap::new())); - assert_eq_tool_names(&tools, &["shell", "update_plan", "web_search"]); + assert_eq_tool_names( + &tools, + &["shell", "update_plan", "web_search", "view_image"], + ); } #[test] @@ -638,6 +679,7 @@ mod tests { include_apply_patch_tool: false, include_web_search_request: true, use_streamable_shell_tool: false, + include_view_image_tool: true, }); let tools = get_openai_tools( &config, @@ -679,11 +721,16 @@ mod tests { assert_eq_tool_names( &tools, - &["shell", "web_search", "test_server/do_something_cool"], + &[ + "shell", + "web_search", + "view_image", + "test_server/do_something_cool", + ], ); assert_eq!( - tools[2], + tools[3], OpenAiTool::Function(ResponsesApiTool { name: "test_server/do_something_cool".to_string(), parameters: JsonSchema::Object { @@ -737,6 +784,7 @@ mod tests { include_apply_patch_tool: false, include_web_search_request: false, use_streamable_shell_tool: false, + include_view_image_tool: true, }); // Intentionally construct a map with keys that would sort alphabetically. @@ -794,6 +842,7 @@ mod tests { &tools, &[ "shell", + "view_image", "test_server/cool", "test_server/do", "test_server/something", @@ -812,6 +861,7 @@ mod tests { include_apply_patch_tool: false, include_web_search_request: true, use_streamable_shell_tool: false, + include_view_image_tool: true, }); let tools = get_openai_tools( @@ -837,10 +887,13 @@ mod tests { )])), ); - assert_eq_tool_names(&tools, &["shell", "web_search", "dash/search"]); + assert_eq_tool_names( + &tools, + &["shell", "web_search", "view_image", "dash/search"], + ); assert_eq!( - tools[2], + tools[3], OpenAiTool::Function(ResponsesApiTool { name: "dash/search".to_string(), parameters: JsonSchema::Object { @@ -870,6 +923,7 @@ mod tests { include_apply_patch_tool: false, include_web_search_request: true, use_streamable_shell_tool: false, + include_view_image_tool: true, }); let tools = get_openai_tools( @@ -893,9 +947,12 @@ mod tests { )])), ); - assert_eq_tool_names(&tools, &["shell", "web_search", "dash/paginate"]); + assert_eq_tool_names( + &tools, + &["shell", "web_search", "view_image", "dash/paginate"], + ); assert_eq!( - tools[2], + tools[3], OpenAiTool::Function(ResponsesApiTool { name: "dash/paginate".to_string(), parameters: JsonSchema::Object { @@ -923,6 +980,7 @@ mod tests { include_apply_patch_tool: false, include_web_search_request: true, use_streamable_shell_tool: false, + include_view_image_tool: true, }); let tools = get_openai_tools( @@ -946,9 +1004,9 @@ mod tests { )])), ); - assert_eq_tool_names(&tools, &["shell", "web_search", "dash/tags"]); + assert_eq_tool_names(&tools, &["shell", "web_search", "view_image", "dash/tags"]); assert_eq!( - tools[2], + tools[3], OpenAiTool::Function(ResponsesApiTool { name: "dash/tags".to_string(), parameters: JsonSchema::Object { @@ -979,6 +1037,7 @@ mod tests { include_apply_patch_tool: false, include_web_search_request: true, use_streamable_shell_tool: false, + include_view_image_tool: true, }); let tools = get_openai_tools( @@ -1002,9 +1061,9 @@ mod tests { )])), ); - assert_eq_tool_names(&tools, &["shell", "web_search", "dash/value"]); + assert_eq_tool_names(&tools, &["shell", "web_search", "view_image", "dash/value"]); assert_eq!( - tools[2], + tools[3], OpenAiTool::Function(ResponsesApiTool { name: "dash/value".to_string(), parameters: JsonSchema::Object { diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 68605ab44f..b165c0bca5 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -191,7 +191,7 @@ async fn prompt_tools_are_consistent_across_requests() { let expected_instructions: &str = include_str!("../../prompt.md"); // our internal implementation is responsible for keeping tools in sync // with the OpenAI schema, so we just verify the tool presence here - let expected_tools_names: &[&str] = &["shell", "update_plan", "apply_patch"]; + let expected_tools_names: &[&str] = &["shell", "update_plan", "apply_patch", "view_image"]; let body0 = requests[0].body_json::().unwrap(); assert_eq!( body0["instructions"], diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 3de95291d1..785272a692 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -148,6 +148,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any base_instructions: None, include_plan_tool: None, include_apply_patch_tool: None, + include_view_image_tool: None, disable_response_storage: oss.then_some(true), show_raw_agent_reasoning: oss.then_some(true), tools_web_search_request: None, diff --git a/codex-rs/mcp-server/src/codex_message_processor.rs b/codex-rs/mcp-server/src/codex_message_processor.rs index 1623e766db..aae463ad92 100644 --- a/codex-rs/mcp-server/src/codex_message_processor.rs +++ b/codex-rs/mcp-server/src/codex_message_processor.rs @@ -798,6 +798,7 @@ fn derive_config_from_params( base_instructions, include_plan_tool, include_apply_patch_tool, + include_view_image_tool: None, disable_response_storage: None, show_raw_agent_reasoning: None, tools_web_search_request: None, diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs index 69f07ff223..c29cb52c22 100644 --- a/codex-rs/mcp-server/src/codex_tool_config.rs +++ b/codex-rs/mcp-server/src/codex_tool_config.rs @@ -161,6 +161,7 @@ impl CodexToolCallParam { base_instructions, include_plan_tool, include_apply_patch_tool: None, + include_view_image_tool: None, disable_response_storage: None, show_raw_agent_reasoning: None, tools_web_search_request: None, diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index e435247891..4154160d80 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -128,6 +128,7 @@ pub async fn run_main( base_instructions: None, include_plan_tool: Some(true), include_apply_patch_tool: None, + include_view_image_tool: None, disable_response_storage: cli.oss.then_some(true), show_raw_agent_reasoning: cli.oss.then_some(true), tools_web_search_request: cli.web_search.then_some(true), From e5611aab07e31880b4962a01bf96b3c02b712c1c Mon Sep 17 00:00:00 2001 From: Jeremy Rose <172423086+nornagon-openai@users.noreply.github.com> Date: Thu, 28 Aug 2025 10:15:59 -0700 Subject: [PATCH 0376/1309] disallow some slash commands while a task is running (#2792) /new, /init, /models, /approvals, etc. don't work correctly during a turn. disable them. --- codex-rs/tui/src/chatwidget.rs | 10 +++++++++- codex-rs/tui/src/slash_command.rs | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index a664e1366d..e687fc038f 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -751,12 +751,20 @@ impl ChatWidget { } fn dispatch_command(&mut self, cmd: SlashCommand) { + if !cmd.available_during_task() && self.bottom_pane.is_task_running() { + let message = format!( + "'/'{}' is disabled while a task is in progress.", + cmd.command() + ); + self.add_to_history(history_cell::new_error_event(message)); + self.request_redraw(); + return; + } match cmd { SlashCommand::New => { self.app_event_tx.send(AppEvent::NewSession); } SlashCommand::Init => { - // Guard: do not run if a task is active. const INIT_PROMPT: &str = include_str!("../prompt_for_init_command.md"); self.submit_text_message(INIT_PROMPT.to_string()); } diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index 6311661b28..c266c4746a 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -52,6 +52,24 @@ impl SlashCommand { pub fn command(self) -> &'static str { self.into() } + + /// Whether this command can be run while a task is in progress. + pub fn available_during_task(self) -> bool { + match self { + SlashCommand::New + | SlashCommand::Init + | SlashCommand::Compact + | SlashCommand::Model + | SlashCommand::Approvals + | SlashCommand::Logout => false, + SlashCommand::Diff + | SlashCommand::Mention + | SlashCommand::Status + | SlashCommand::Mcp + | SlashCommand::Quit + | SlashCommand::TestApproval => true, + } + } } /// Return all built-in commands in a Vec paired with their command string. From 74d2741729b4e4aee7e34ddf8c30de03e258250b Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 28 Aug 2025 11:25:23 -0700 Subject: [PATCH 0377/1309] chore: require uninlined_format_args from clippy (#2845) - added `uninlined_format_args` to `[workspace.lints.clippy]` in the `Cargo.toml` for the workspace - ran `cargo clippy --tests --fix` - ran `just fmt` --- codex-rs/Cargo.toml | 1 + codex-rs/core/src/config.rs | 8 ++++---- codex-rs/core/src/environment_context.rs | 10 ++++------ codex-rs/core/src/error.rs | 6 +++--- .../core/src/exec_command/session_manager.rs | 19 +++++-------------- codex-rs/core/tests/suite/client.rs | 4 ++-- codex-rs/core/tests/suite/prompt_caching.rs | 2 +- codex-rs/exec/tests/suite/common.rs | 4 ++-- codex-rs/mcp-server/src/outgoing_message.rs | 2 +- 9 files changed, 23 insertions(+), 33 deletions(-) diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 8a48ef8187..4155992293 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -34,6 +34,7 @@ rust = {} [workspace.lints.clippy] expect_used = "deny" +uninlined_format_args = "deny" unwrap_used = "deny" [profile.release] diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 9b8f288cf3..4d623c3e5b 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -1317,9 +1317,9 @@ disable_response_storage = true let raw_path = project_dir.path().to_string_lossy(); let path_str = if raw_path.contains('\\') { - format!("'{}'", raw_path) + format!("'{raw_path}'") } else { - format!("\"{}\"", raw_path) + format!("\"{raw_path}\"") }; let expected = format!( r#"[projects.{path_str}] @@ -1340,9 +1340,9 @@ trust_level = "trusted" let config_path = codex_home.path().join(CONFIG_TOML_FILE); let raw_path = project_dir.path().to_string_lossy(); let path_str = if raw_path.contains('\\') { - format!("'{}'", raw_path) + format!("'{raw_path}'") } else { - format!("\"{}\"", raw_path) + format!("\"{raw_path}\"") }; // Use a quoted key so backslashes don't require escaping on Windows let initial = format!( diff --git a/codex-rs/core/src/environment_context.rs b/codex-rs/core/src/environment_context.rs index 1af4c9098a..b7ee862517 100644 --- a/codex-rs/core/src/environment_context.rs +++ b/codex-rs/core/src/environment_context.rs @@ -85,23 +85,21 @@ impl EnvironmentContext { } if let Some(approval_policy) = self.approval_policy { lines.push(format!( - " {}", - approval_policy + " {approval_policy}" )); } if let Some(sandbox_mode) = self.sandbox_mode { - lines.push(format!(" {}", sandbox_mode)); + lines.push(format!(" {sandbox_mode}")); } if let Some(network_access) = self.network_access { lines.push(format!( - " {}", - network_access + " {network_access}" )); } if let Some(shell) = self.shell && let Some(shell_name) = shell.name() { - lines.push(format!(" {}", shell_name)); + lines.push(format!(" {shell_name}")); } lines.push(ENVIRONMENT_CONTEXT_END.to_string()); lines.join("\n") diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index b05ff1a581..00ac145c2e 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -170,15 +170,15 @@ fn format_reset_duration(total_secs: u64) -> String { let mut parts: Vec = Vec::new(); if days > 0 { let unit = if days == 1 { "day" } else { "days" }; - parts.push(format!("{} {}", days, unit)); + parts.push(format!("{days} {unit}")); } if hours > 0 { let unit = if hours == 1 { "hour" } else { "hours" }; - parts.push(format!("{} {}", hours, unit)); + parts.push(format!("{hours} {unit}")); } if minutes > 0 { let unit = if minutes == 1 { "minute" } else { "minutes" }; - parts.push(format!("{} {}", minutes, unit)); + parts.push(format!("{minutes} {unit}")); } if parts.is_empty() { diff --git a/codex-rs/core/src/exec_command/session_manager.rs b/codex-rs/core/src/exec_command/session_manager.rs index 5359024bdd..c547409cd1 100644 --- a/codex-rs/core/src/exec_command/session_manager.rs +++ b/codex-rs/core/src/exec_command/session_manager.rs @@ -359,10 +359,7 @@ fn truncate_middle(s: &str, max_bytes: usize) -> (String, Option) { let est_tokens = (s.len() as u64).div_ceil(4); if max_bytes == 0 { // Cannot keep any content; still return a full marker (never truncated). - return ( - format!("…{} tokens truncated…", est_tokens), - Some(est_tokens), - ); + return (format!("…{est_tokens} tokens truncated…"), Some(est_tokens)); } // Helper to truncate a string to a given byte length on a char boundary. @@ -406,16 +403,13 @@ fn truncate_middle(s: &str, max_bytes: usize) -> (String, Option) { // Refine marker length and budgets until stable. Marker is never truncated. let mut guess_tokens = est_tokens; // worst-case: everything truncated for _ in 0..4 { - let marker = format!("…{} tokens truncated…", guess_tokens); + let marker = format!("…{guess_tokens} tokens truncated…"); let marker_len = marker.len(); let keep_budget = max_bytes.saturating_sub(marker_len); if keep_budget == 0 { // No room for any content within the cap; return a full, untruncated marker // that reflects the entire truncated content. - return ( - format!("…{} tokens truncated…", est_tokens), - Some(est_tokens), - ); + return (format!("…{est_tokens} tokens truncated…"), Some(est_tokens)); } let left_budget = keep_budget / 2; @@ -441,14 +435,11 @@ fn truncate_middle(s: &str, max_bytes: usize) -> (String, Option) { } // Fallback: use last guess to build output. - let marker = format!("…{} tokens truncated…", guess_tokens); + let marker = format!("…{guess_tokens} tokens truncated…"); let marker_len = marker.len(); let keep_budget = max_bytes.saturating_sub(marker_len); if keep_budget == 0 { - return ( - format!("…{} tokens truncated…", est_tokens), - Some(est_tokens), - ); + return (format!("…{est_tokens} tokens truncated…"), Some(est_tokens)); } let left_budget = keep_budget / 2; let right_budget = keep_budget - left_budget; diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 5a1fb35b12..aed34dc3a1 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -418,7 +418,7 @@ async fn prefers_chatgpt_token_when_config_prefers_chatgpt() { match CodexAuth::from_codex_home(codex_home.path(), config.preferred_auth_method) { Ok(Some(auth)) => codex_login::AuthManager::from_auth_for_testing(auth), Ok(None) => panic!("No CodexAuth found in codex_home"), - Err(e) => panic!("Failed to load CodexAuth: {}", e), + Err(e) => panic!("Failed to load CodexAuth: {e}"), }; let conversation_manager = ConversationManager::new(auth_manager); let NewConversation { @@ -499,7 +499,7 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() { match CodexAuth::from_codex_home(codex_home.path(), config.preferred_auth_method) { Ok(Some(auth)) => codex_login::AuthManager::from_auth_for_testing(auth), Ok(None) => panic!("No CodexAuth found in codex_home"), - Err(e) => panic!("Failed to load CodexAuth: {}", e), + Err(e) => panic!("Failed to load CodexAuth: {e}"), }; let conversation_manager = ConversationManager::new(auth_manager); let NewConversation { diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index b165c0bca5..999f807286 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -280,7 +280,7 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests {}
"#, cwd.path().to_string_lossy(), match shell.name() { - Some(name) => format!(" {}\n", name), + Some(name) => format!(" {name}\n"), None => String::new(), } ); diff --git a/codex-rs/exec/tests/suite/common.rs b/codex-rs/exec/tests/suite/common.rs index 49747dca05..8c57e7afcb 100644 --- a/codex-rs/exec/tests/suite/common.rs +++ b/codex-rs/exec/tests/suite/common.rs @@ -28,7 +28,7 @@ impl Respond for SeqResponder { Some(body) => wiremock::ResponseTemplate::new(200) .insert_header("content-type", "text/event-stream") .set_body_raw( - load_sse_fixture_with_id_from_str(body, &format!("request_{}", call_num)), + load_sse_fixture_with_id_from_str(body, &format!("request_{call_num}")), "text/event-stream", ), None => panic!("no response for {call_num}"), @@ -63,7 +63,7 @@ pub(crate) async fn run_e2e_exec_test(cwd: &Path, response_streams: Vec) .current_dir(cwd.clone()) .env("CODEX_HOME", cwd.clone()) .env("OPENAI_API_KEY", "dummy") - .env("OPENAI_BASE_URL", format!("{}/v1", uri)) + .env("OPENAI_BASE_URL", format!("{uri}/v1")) .arg("--skip-git-repo-check") .arg("-s") .arg("danger-full-access") diff --git a/codex-rs/mcp-server/src/outgoing_message.rs b/codex-rs/mcp-server/src/outgoing_message.rs index 16241a0899..5f206cb0cb 100644 --- a/codex-rs/mcp-server/src/outgoing_message.rs +++ b/codex-rs/mcp-server/src/outgoing_message.rs @@ -123,7 +123,7 @@ impl OutgoingMessageSender { } pub(crate) async fn send_server_notification(&self, notification: ServerNotification) { - let method = format!("codex/event/{}", notification); + let method = format!("codex/event/{notification}"); let params = match serde_json::to_value(¬ification) { Ok(serde_json::Value::Object(mut map)) => map.remove("data"), _ => None, From 1e9e703b969d3f0965b31d1cc3d70fed3ebdd6f6 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 28 Aug 2025 12:33:33 -0700 Subject: [PATCH 0378/1309] chore: try to make it easier to debug the flakiness of test_shell_command_approval_triggers_elicitation (#2848) `test_shell_command_approval_triggers_elicitation()` is one of a number of integration tests that we have observed to be flaky on GitHub CI, so this PR tries to reduce the flakiness _and_ to provide us with more information when it flakes. Specifically: - Changed the command that we use to trigger the elicitation from `git init` to `python3 -c 'import pathlib; pathlib.Path(r"{}").touch()'` because running `git` seems more likely to invite variance. - Increased the timeout to wait for the task response from 10s to 20s. - Added more logging. --- .../mcp-server/tests/common/mcp_process.rs | 21 ++++++------ codex-rs/mcp-server/tests/suite/codex_tool.rs | 32 ++++++++++++------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/codex-rs/mcp-server/tests/common/mcp_process.rs b/codex-rs/mcp-server/tests/common/mcp_process.rs index 5788163c10..eae83482d7 100644 --- a/codex-rs/mcp-server/tests/common/mcp_process.rs +++ b/codex-rs/mcp-server/tests/common/mcp_process.rs @@ -283,6 +283,7 @@ impl McpProcess { } async fn send_jsonrpc_message(&mut self, message: JSONRPCMessage) -> anyhow::Result<()> { + eprintln!("writing message to stdin: {message:?}"); let payload = serde_json::to_string(&message)?; self.stdin.write_all(payload.as_bytes()).await?; self.stdin.write_all(b"\n").await?; @@ -294,13 +295,15 @@ impl McpProcess { let mut line = String::new(); self.stdout.read_line(&mut line).await?; let message = serde_json::from_str::(&line)?; + eprintln!("read message from stdout: {message:?}"); Ok(message) } pub async fn read_stream_until_request_message(&mut self) -> anyhow::Result { + eprintln!("in read_stream_until_request_message()"); + loop { let message = self.read_jsonrpc_message().await?; - eprint!("message: {message:?}"); match message { JSONRPCMessage::Notification(_) => { @@ -323,10 +326,10 @@ impl McpProcess { &mut self, request_id: RequestId, ) -> anyhow::Result { + eprintln!("in read_stream_until_response_message({request_id:?})"); + loop { let message = self.read_jsonrpc_message().await?; - eprint!("message: {message:?}"); - match message { JSONRPCMessage::Notification(_) => { eprintln!("notification: {message:?}"); @@ -352,8 +355,6 @@ impl McpProcess { ) -> anyhow::Result { loop { let message = self.read_jsonrpc_message().await?; - eprint!("message: {message:?}"); - match message { JSONRPCMessage::Notification(_) => { eprintln!("notification: {message:?}"); @@ -377,10 +378,10 @@ impl McpProcess { &mut self, method: &str, ) -> anyhow::Result { + eprintln!("in read_stream_until_notification_message({method})"); + loop { let message = self.read_jsonrpc_message().await?; - eprint!("message: {message:?}"); - match message { JSONRPCMessage::Notification(notification) => { if notification.method == method { @@ -405,10 +406,10 @@ impl McpProcess { pub async fn read_stream_until_legacy_task_complete_notification( &mut self, ) -> anyhow::Result { + eprintln!("in read_stream_until_legacy_task_complete_notification()"); + loop { let message = self.read_jsonrpc_message().await?; - eprint!("message: {message:?}"); - match message { JSONRPCMessage::Notification(notification) => { let is_match = if notification.method == "codex/event" { @@ -427,6 +428,8 @@ impl McpProcess { if is_match { return Ok(notification); + } else { + eprintln!("ignoring notification: {notification:?}"); } } JSONRPCMessage::Request(_) => { diff --git a/codex-rs/mcp-server/tests/suite/codex_tool.rs b/codex-rs/mcp-server/tests/suite/codex_tool.rs index 13866d970c..e7097b6b34 100644 --- a/codex-rs/mcp-server/tests/suite/codex_tool.rs +++ b/codex-rs/mcp-server/tests/suite/codex_tool.rs @@ -30,7 +30,8 @@ use mcp_test_support::create_final_assistant_message_sse_response; use mcp_test_support::create_mock_chat_completions_server; use mcp_test_support::create_shell_sse_response; -const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); +// Allow ample time on slower CI or under load to avoid flakes. +const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(20); /// Test that a shell command that is not on the "trusted" list triggers an /// elicitation request to the MCP and that sending the approval runs the @@ -52,9 +53,22 @@ async fn test_shell_command_approval_triggers_elicitation() { } async fn shell_command_approval_triggers_elicitation() -> anyhow::Result<()> { - // We use `git init` because it will not be on the "trusted" list. - let shell_command = vec!["git".to_string(), "init".to_string()]; + // Use a simple, untrusted command that creates a file so we can + // observe a side-effect. + // + // Cross‑platform approach: run a tiny Python snippet to touch the file + // using `python3 -c ...` on all platforms. let workdir_for_shell_function_call = TempDir::new()?; + let created_filename = "created_by_shell_tool.txt"; + let created_file = workdir_for_shell_function_call + .path() + .join(created_filename); + + let shell_command = vec![ + "python3".to_string(), + "-c".to_string(), + format!("import pathlib; pathlib.Path('{created_filename}').touch()"), + ]; let McpHandle { process: mut mcp_process, @@ -67,7 +81,7 @@ async fn shell_command_approval_triggers_elicitation() -> anyhow::Result<()> { Some(5_000), "call1234", )?, - create_final_assistant_message_sse_response("Enjoy your new git repo!")?, + create_final_assistant_message_sse_response("File created!")?, ]) .await?; @@ -122,8 +136,7 @@ async fn shell_command_approval_triggers_elicitation() -> anyhow::Result<()> { .expect("task_complete_notification timeout") .expect("task_complete_notification resp"); - // Verify the original `codex` tool call completes and that `git init` ran - // successfully. + // Verify the original `codex` tool call completes and that the file was created. let codex_response = timeout( DEFAULT_READ_TIMEOUT, mcp_process.read_stream_until_response_message(RequestId::Integer(codex_request_id)), @@ -136,7 +149,7 @@ async fn shell_command_approval_triggers_elicitation() -> anyhow::Result<()> { result: json!({ "content": [ { - "text": "Enjoy your new git repo!", + "text": "File created!", "type": "text" } ] @@ -145,10 +158,7 @@ async fn shell_command_approval_triggers_elicitation() -> anyhow::Result<()> { codex_response ); - assert!( - workdir_for_shell_function_call.path().join(".git").is_dir(), - ".git folder should have been created" - ); + assert!(created_file.is_file(), "created file should exist"); Ok(()) } From f09170b574b52003b0746e18346b9f5fc4007464 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 28 Aug 2025 12:43:13 -0700 Subject: [PATCH 0379/1309] chore: print stderr from MCP server to test output using eprintln! (#2849) Related to https://github.com/openai/codex/pull/2848, I don't see the stderr from `codex mcp` colocated with the other stderr from `test_shell_command_approval_triggers_elicitation()` when it fails even though we have `RUST_LOG=debug` set when we spawn `codex mcp`: https://github.com/openai/codex/blob/1e9e703b969d3f0965b31d1cc3d70fed3ebdd6f6/codex-rs/mcp-server/tests/common/mcp_process.rs#L65 Let's try this new logic which should be more explicit. --- codex-rs/mcp-server/tests/common/mcp_process.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/codex-rs/mcp-server/tests/common/mcp_process.rs b/codex-rs/mcp-server/tests/common/mcp_process.rs index eae83482d7..14939156bb 100644 --- a/codex-rs/mcp-server/tests/common/mcp_process.rs +++ b/codex-rs/mcp-server/tests/common/mcp_process.rs @@ -61,6 +61,7 @@ impl McpProcess { cmd.stdin(Stdio::piped()); cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); cmd.env("CODEX_HOME", codex_home); cmd.env("RUST_LOG", "debug"); @@ -77,6 +78,17 @@ impl McpProcess { .take() .ok_or_else(|| anyhow::format_err!("mcp should have stdout fd"))?; let stdout = BufReader::new(stdout); + + // Forward child's stderr to our stderr so failures are visible even + // when stdout/stderr are captured by the test harness. + if let Some(stderr) = process.stderr.take() { + let mut stderr_reader = BufReader::new(stderr).lines(); + tokio::spawn(async move { + while let Ok(Some(line)) = stderr_reader.next_line().await { + eprintln!("[mcp stderr] {line}"); + } + }); + } Ok(Self { next_request_id: AtomicI64::new(0), process, From ed06f90fb3639775a9e2f727c2393464704d580e Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Thu, 28 Aug 2025 12:53:00 -0700 Subject: [PATCH 0380/1309] Race condition in compact (#2746) This fixes the flakiness in `summarize_context_three_requests_and_instructions` because we should trim history before sending task complete. --- codex-rs/core/src/codex.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 365969ac02..8443c534fb 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1884,6 +1884,12 @@ async fn run_compact_task( } sess.remove_task(&sub_id); + + { + let mut state = sess.state.lock_unchecked(); + state.history.keep_last_messages(1); + } + let event = Event { id: sub_id.clone(), msg: EventMsg::AgentMessage(AgentMessageEvent { @@ -1898,9 +1904,6 @@ async fn run_compact_task( }), }; sess.send_event(event).await; - - let mut state = sess.state.lock_unchecked(); - state.history.keep_last_messages(1); } async fn handle_response_item( From c9ca63dc1e7ff89abcf6d4972561c20f9a1f11e3 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Thu, 28 Aug 2025 12:54:12 -0700 Subject: [PATCH 0381/1309] burst paste edge cases (#2683) This PR fixes two edge cases in managing burst paste (mainly on power shell). Bugs: - Needs an event key after paste to render the pasted items > ChatComposer::flush_paste_burst_if_due() flushes on timeout. Called: > - Pre-render in App on TuiEvent::Draw. > - Via a delayed frame > BottomPane::request_redraw_in(ChatComposer::recommended_paste_flush_delay()). - Parses two key events separately before starting parsing burst paste > When threshold is crossed, pull preceding burst chars out of the textarea and prepend to paste_burst_buffer, then keep buffering. - Integrates with #2567 to bring image pasting to windows. --- codex-rs/core/src/config.rs | 13 + codex-rs/tui/src/app.rs | 6 + .../src/bottom_pane/approval_modal_view.rs | 1 + codex-rs/tui/src/bottom_pane/chat_composer.rs | 480 ++++++++++++------ codex-rs/tui/src/bottom_pane/mod.rs | 25 + codex-rs/tui/src/bottom_pane/paste_burst.rs | 246 +++++++++ codex-rs/tui/src/chatwidget.rs | 20 + codex-rs/tui/src/chatwidget/tests.rs | 1 + 8 files changed, 633 insertions(+), 159 deletions(-) create mode 100644 codex-rs/tui/src/bottom_pane/paste_burst.rs diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 4d623c3e5b..b47f717c57 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -181,6 +181,10 @@ pub struct Config { /// Include the `view_image` tool that lets the agent attach a local image path to context. pub include_view_image_tool: bool, + /// When true, disables burst-paste detection for typed input entirely. + /// All characters are inserted as they are received, and no buffering + /// or placeholder replacement will occur for fast keypress bursts. + pub disable_paste_burst: bool, } impl Config { @@ -488,6 +492,11 @@ pub struct ConfigToml { /// Nested tools section for feature toggles pub tools: Option, + + /// When true, disables burst-paste detection for typed input entirely. + /// All characters are inserted as they are received, and no buffering + /// or placeholder replacement will occur for fast keypress bursts. + pub disable_paste_burst: Option, } #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] @@ -798,6 +807,7 @@ impl Config { .experimental_use_exec_command_tool .unwrap_or(false), include_view_image_tool, + disable_paste_burst: cfg.disable_paste_burst.unwrap_or(false), }; Ok(config) } @@ -1167,6 +1177,7 @@ disable_response_storage = true preferred_auth_method: AuthMode::ChatGPT, use_experimental_streamable_shell_tool: false, include_view_image_tool: true, + disable_paste_burst: false, }, o3_profile_config ); @@ -1224,6 +1235,7 @@ disable_response_storage = true preferred_auth_method: AuthMode::ChatGPT, use_experimental_streamable_shell_tool: false, include_view_image_tool: true, + disable_paste_burst: false, }; assert_eq!(expected_gpt3_profile_config, gpt3_profile_config); @@ -1296,6 +1308,7 @@ disable_response_storage = true preferred_auth_method: AuthMode::ChatGPT, use_experimental_streamable_shell_tool: false, include_view_image_tool: true, + disable_paste_burst: false, }; assert_eq!(expected_zdr_profile_config, zdr_profile_config); diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 6c978e277f..6d107d67a0 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -133,6 +133,12 @@ impl App { self.chat_widget.handle_paste(pasted); } TuiEvent::Draw => { + if self + .chat_widget + .handle_paste_burst_tick(tui.frame_requester()) + { + return Ok(true); + } tui.draw( self.chat_widget.desired_height(tui.terminal.size()?.width), |frame| { diff --git a/codex-rs/tui/src/bottom_pane/approval_modal_view.rs b/codex-rs/tui/src/bottom_pane/approval_modal_view.rs index 518d9d0351..e204051d28 100644 --- a/codex-rs/tui/src/bottom_pane/approval_modal_view.rs +++ b/codex-rs/tui/src/bottom_pane/approval_modal_view.rs @@ -100,6 +100,7 @@ mod tests { has_input_focus: true, enhanced_keys_supported: false, placeholder_text: "Ask Codex to do anything".to_string(), + disable_paste_burst: false, }); assert_eq!(CancellationEvent::Handled, view.on_ctrl_c(&mut pane)); assert!(view.queue.is_empty()); diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 26b9a571af..c21d5a4b5d 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -24,6 +24,8 @@ use ratatui::widgets::WidgetRef; use super::chat_composer_history::ChatComposerHistory; use super::command_popup::CommandPopup; use super::file_search_popup::FileSearchPopup; +use super::paste_burst::CharDecision; +use super::paste_burst::PasteBurst; use crate::slash_command::SlashCommand; use crate::app_event::AppEvent; @@ -40,11 +42,6 @@ use std::path::PathBuf; use std::time::Duration; use std::time::Instant; -// Heuristic thresholds for detecting paste-like input bursts. -const PASTE_BURST_MIN_CHARS: u16 = 3; -const PASTE_BURST_CHAR_INTERVAL: Duration = Duration::from_millis(8); -const PASTE_ENTER_SUPPRESS_WINDOW: Duration = Duration::from_millis(120); - /// If the pasted content exceeds this number of characters, replace it with a /// placeholder in the UI. const LARGE_PASTE_CHAR_THRESHOLD: usize = 1000; @@ -93,13 +90,10 @@ pub(crate) struct ChatComposer { has_focus: bool, attached_images: Vec, placeholder_text: String, - // Heuristic state to detect non-bracketed paste bursts. - last_plain_char_time: Option, - consecutive_plain_char_burst: u16, - paste_burst_until: Option, - // Buffer to accumulate characters during a detected non-bracketed paste burst. - paste_burst_buffer: String, - in_paste_burst_mode: bool, + // Non-bracketed paste burst tracker. + paste_burst: PasteBurst, + // When true, disables paste-burst logic and inserts characters immediately. + disable_paste_burst: bool, } /// Popup state – at most one can be visible at any time. @@ -115,10 +109,11 @@ impl ChatComposer { app_event_tx: AppEventSender, enhanced_keys_supported: bool, placeholder_text: String, + disable_paste_burst: bool, ) -> Self { let use_shift_enter_hint = enhanced_keys_supported; - Self { + let mut this = Self { textarea: TextArea::new(), textarea_state: RefCell::new(TextAreaState::default()), active_popup: ActivePopup::None, @@ -134,12 +129,12 @@ impl ChatComposer { has_focus: has_input_focus, attached_images: Vec::new(), placeholder_text, - last_plain_char_time: None, - consecutive_plain_char_burst: 0, - paste_burst_until: None, - paste_burst_buffer: String::new(), - in_paste_burst_mode: false, - } + paste_burst: PasteBurst::default(), + disable_paste_burst: false, + }; + // Apply configuration via the setter to keep side-effects centralized. + this.set_disable_paste_burst(disable_paste_burst); + this } pub fn desired_height(&self, width: u16) -> u16 { @@ -229,11 +224,15 @@ impl ChatComposer { self.textarea.insert_str(&pasted); } // Explicit paste events should not trigger Enter suppression. - self.last_plain_char_time = None; - self.consecutive_plain_char_burst = 0; - self.paste_burst_until = None; + self.paste_burst.clear_after_explicit_paste(); + // Keep popup sync consistent with key handling: prefer slash popup; only + // sync file popup when slash popup is NOT active. self.sync_command_popup(); - self.sync_file_search_popup(); + if matches!(self.active_popup, ActivePopup::Command(_)) { + self.dismissed_file_popup_token = None; + } else { + self.sync_file_search_popup(); + } true } @@ -256,6 +255,14 @@ impl ChatComposer { } } + pub(crate) fn set_disable_paste_burst(&mut self, disabled: bool) { + let was_disabled = self.disable_paste_burst; + self.disable_paste_burst = disabled; + if disabled && !was_disabled { + self.paste_burst.clear_window_after_non_char(); + } + } + /// Replace the entire composer content with `text` and reset cursor. pub(crate) fn set_text_content(&mut self, text: String) { self.textarea.set_text(&text); @@ -270,6 +277,7 @@ impl ChatComposer { self.textarea.text().to_string() } + /// Attempt to start a burst by retro-capturing recent chars before the cursor. pub fn attach_image(&mut self, path: PathBuf, width: u32, height: u32, format_label: &str) { let placeholder = format!("[image {width}x{height} {format_label}]"); // Insert as an element to match large paste placeholder behavior: @@ -284,6 +292,23 @@ impl ChatComposer { images.into_iter().map(|img| img.path).collect() } + pub(crate) fn flush_paste_burst_if_due(&mut self) -> bool { + let now = Instant::now(); + if let Some(pasted) = self.paste_burst.flush_if_due(now) { + let _ = self.handle_paste(pasted); + return true; + } + false + } + + pub(crate) fn is_in_paste_burst(&self) -> bool { + self.paste_burst.is_active() + } + + pub(crate) fn recommended_paste_flush_delay() -> Duration { + PasteBurst::recommended_flush_delay() + } + /// Integrate results from an asynchronous file search. pub(crate) fn on_file_search_result(&mut self, query: String, matches: Vec) { // Only apply if user is still editing a token starting with `query`. @@ -423,9 +448,7 @@ impl ChatComposer { #[inline] fn handle_non_ascii_char(&mut self, input: KeyEvent) -> (InputResult, bool) { - if !self.paste_burst_buffer.is_empty() || self.in_paste_burst_mode { - let pasted = std::mem::take(&mut self.paste_burst_buffer); - self.in_paste_burst_mode = false; + if let Some(pasted) = self.paste_burst.flush_before_modified_input() { self.handle_paste(pasted); } self.textarea.input(input); @@ -740,14 +763,11 @@ impl ChatComposer { .next() .unwrap_or("") .starts_with('/'); - if (self.in_paste_burst_mode || !self.paste_burst_buffer.is_empty()) - && !in_slash_context - { - self.paste_burst_buffer.push('\n'); + if self.paste_burst.is_active() && !in_slash_context { let now = Instant::now(); - // Keep the window alive so subsequent lines are captured too. - self.paste_burst_until = Some(now + PASTE_ENTER_SUPPRESS_WINDOW); - return (InputResult::None, true); + if self.paste_burst.append_newline_if_active(now) { + return (InputResult::None, true); + } } // If we have pending placeholder pastes, submit immediately to expand them. if !self.pending_pastes.is_empty() { @@ -768,19 +788,12 @@ impl ChatComposer { // During a paste-like burst, treat Enter as a newline instead of submit. let now = Instant::now(); - let tight_after_char = self - .last_plain_char_time - .is_some_and(|t| now.duration_since(t) <= PASTE_BURST_CHAR_INTERVAL); - let recent_after_char = self - .last_plain_char_time - .is_some_and(|t| now.duration_since(t) <= PASTE_ENTER_SUPPRESS_WINDOW); - let burst_by_count = - recent_after_char && self.consecutive_plain_char_burst >= PASTE_BURST_MIN_CHARS; - let in_burst_window = self.paste_burst_until.is_some_and(|until| now <= until); - - if tight_after_char || burst_by_count || in_burst_window { + if self + .paste_burst + .newline_should_insert_instead_of_submit(now) + { self.textarea.insert_str("\n"); - self.paste_burst_until = Some(now + PASTE_ENTER_SUPPRESS_WINDOW); + self.paste_burst.extend_window(now); return (InputResult::None, true); } let mut text = self.textarea.text().to_string(); @@ -810,22 +823,16 @@ impl ChatComposer { // If we have a buffered non-bracketed paste burst and enough time has // elapsed since the last char, flush it before handling a new input. let now = Instant::now(); - let timed_out = self - .last_plain_char_time - .is_some_and(|t| now.duration_since(t) > PASTE_BURST_CHAR_INTERVAL); - if timed_out && (!self.paste_burst_buffer.is_empty() || self.in_paste_burst_mode) { - let pasted = std::mem::take(&mut self.paste_burst_buffer); - self.in_paste_burst_mode = false; + if let Some(pasted) = self.paste_burst.flush_if_due(now) { // Reuse normal paste path (handles large-paste placeholders). self.handle_paste(pasted); } // If we're capturing a burst and receive Enter, accumulate it instead of inserting. if matches!(input.code, KeyCode::Enter) - && (self.in_paste_burst_mode || !self.paste_burst_buffer.is_empty()) + && self.paste_burst.is_active() + && self.paste_burst.append_newline_if_active(now) { - self.paste_burst_buffer.push('\n'); - self.paste_burst_until = Some(now + PASTE_ENTER_SUPPRESS_WINDOW); return (InputResult::None, true); } @@ -840,65 +847,50 @@ impl ChatComposer { modifiers.contains(KeyModifiers::CONTROL) || modifiers.contains(KeyModifiers::ALT); if !has_ctrl_or_alt { // Non-ASCII characters (e.g., from IMEs) can arrive in quick bursts and be - // misclassified by our non-bracketed paste heuristic. To avoid leaving - // residual buffered content or misdetecting a paste, flush any burst buffer - // and insert non-ASCII characters directly. + // misclassified by paste heuristics. Flush any active burst buffer and insert + // non-ASCII characters directly. if !ch.is_ascii() { return self.handle_non_ascii_char(input); } - // Update burst heuristics. - match self.last_plain_char_time { - Some(prev) if now.duration_since(prev) <= PASTE_BURST_CHAR_INTERVAL => { - self.consecutive_plain_char_burst = - self.consecutive_plain_char_burst.saturating_add(1); - } - _ => { - self.consecutive_plain_char_burst = 1; - } - } - self.last_plain_char_time = Some(now); - // If we're already buffering, capture the char into the buffer. - if self.in_paste_burst_mode { - self.paste_burst_buffer.push(ch); - // Keep the window alive while we receive the burst. - self.paste_burst_until = Some(now + PASTE_ENTER_SUPPRESS_WINDOW); - return (InputResult::None, true); - } else if self.consecutive_plain_char_burst >= PASTE_BURST_MIN_CHARS { - // Do not start burst buffering while typing a slash command (first line starts with '/'). - let first_line = self.textarea.text().lines().next().unwrap_or(""); - if first_line.starts_with('/') { - // Keep heuristics but do not buffer. - self.paste_burst_until = Some(now + PASTE_ENTER_SUPPRESS_WINDOW); - // Insert normally. - self.textarea.input(input); - let text_after = self.textarea.text(); - self.pending_pastes - .retain(|(placeholder, _)| text_after.contains(placeholder)); + match self.paste_burst.on_plain_char(ch, now) { + CharDecision::BufferAppend => { + self.paste_burst.append_char_to_buffer(ch, now); + return (InputResult::None, true); + } + CharDecision::BeginBuffer { retro_chars } => { + let cur = self.textarea.cursor(); + let txt = self.textarea.text(); + let safe_cur = Self::clamp_to_char_boundary(txt, cur); + let before = &txt[..safe_cur]; + if let Some(grab) = + self.paste_burst + .decide_begin_buffer(now, before, retro_chars as usize) + { + if !grab.grabbed.is_empty() { + self.textarea.replace_range(grab.start_byte..safe_cur, ""); + } + self.paste_burst.begin_with_retro_grabbed(grab.grabbed, now); + self.paste_burst.append_char_to_buffer(ch, now); + return (InputResult::None, true); + } + // If decide_begin_buffer opted not to start buffering, + // fall through to normal insertion below. + } + CharDecision::BeginBufferFromPending => { + // First char was held; now append the current one. + self.paste_burst.append_char_to_buffer(ch, now); + return (InputResult::None, true); + } + CharDecision::RetainFirstChar => { + // Keep the first fast char pending momentarily. return (InputResult::None, true); } - // Begin buffering from this character onward. - self.paste_burst_buffer.push(ch); - self.in_paste_burst_mode = true; - // Keep the window alive to continue capturing. - self.paste_burst_until = Some(now + PASTE_ENTER_SUPPRESS_WINDOW); - return (InputResult::None, true); - } - - // Not buffering: insert normally and continue. - self.textarea.input(input); - let text_after = self.textarea.text(); - self.pending_pastes - .retain(|(placeholder, _)| text_after.contains(placeholder)); - return (InputResult::None, true); - } else { - // Modified char ends any burst: flush buffered content before applying. - if !self.paste_burst_buffer.is_empty() || self.in_paste_burst_mode { - let pasted = std::mem::take(&mut self.paste_burst_buffer); - self.in_paste_burst_mode = false; - self.handle_paste(pasted); } } + if let Some(pasted) = self.paste_burst.flush_before_modified_input() { + self.handle_paste(pasted); + } } // For non-char inputs (or after flushing), handle normally. @@ -925,25 +917,15 @@ impl ChatComposer { let has_ctrl_or_alt = modifiers.contains(KeyModifiers::CONTROL) || modifiers.contains(KeyModifiers::ALT); if has_ctrl_or_alt { - // Modified char: clear burst window. - self.consecutive_plain_char_burst = 0; - self.last_plain_char_time = None; - self.paste_burst_until = None; - self.in_paste_burst_mode = false; - self.paste_burst_buffer.clear(); + self.paste_burst.clear_window_after_non_char(); } - // Plain chars handled above. } KeyCode::Enter => { // Keep burst window alive (supports blank lines in paste). } _ => { - // Other keys: clear burst window and any buffer (after flushing earlier). - self.consecutive_plain_char_burst = 0; - self.last_plain_char_time = None; - self.paste_burst_until = None; - self.in_paste_burst_mode = false; - // Do not clear paste_burst_buffer here; it should have been flushed above. + // Other keys: clear burst window (buffer should have been flushed above if needed). + self.paste_burst.clear_window_after_non_char(); } } @@ -1480,8 +1462,13 @@ mod tests { let (tx, _rx) = unbounded_channel::(); let sender = AppEventSender::new(tx); - let mut composer = - ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string()); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); let needs_redraw = composer.handle_paste("hello".to_string()); assert!(needs_redraw); @@ -1504,8 +1491,13 @@ mod tests { let (tx, _rx) = unbounded_channel::(); let sender = AppEventSender::new(tx); - let mut composer = - ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string()); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); let large = "x".repeat(LARGE_PASTE_CHAR_THRESHOLD + 10); let needs_redraw = composer.handle_paste(large.clone()); @@ -1534,8 +1526,13 @@ mod tests { let large = "y".repeat(LARGE_PASTE_CHAR_THRESHOLD + 1); let (tx, _rx) = unbounded_channel::(); let sender = AppEventSender::new(tx); - let mut composer = - ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string()); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); composer.handle_paste(large); assert_eq!(composer.pending_pastes.len(), 1); @@ -1576,6 +1573,7 @@ mod tests { sender.clone(), false, "Ask Codex to do anything".to_string(), + false, ); if let Some(text) = input { @@ -1605,6 +1603,18 @@ mod tests { } } + // Test helper: simulate human typing with a brief delay and flush the paste-burst buffer + fn type_chars_humanlike(composer: &mut ChatComposer, chars: &[char]) { + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + for &ch in chars { + let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE)); + std::thread::sleep(ChatComposer::recommended_paste_flush_delay()); + let _ = composer.flush_paste_burst_if_due(); + } + } + #[test] fn slash_init_dispatches_command_and_does_not_submit_literal_text() { use crossterm::event::KeyCode; @@ -1613,15 +1623,16 @@ mod tests { let (tx, _rx) = unbounded_channel::(); let sender = AppEventSender::new(tx); - let mut composer = - ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string()); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); // Type the slash command. - for ch in [ - '/', 'i', 'n', 'i', 't', // "/init" - ] { - let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE)); - } + type_chars_humanlike(&mut composer, &['/', 'i', 'n', 'i', 't']); // Press Enter to dispatch the selected command. let (result, _needs_redraw) = @@ -1649,12 +1660,15 @@ mod tests { let (tx, _rx) = unbounded_channel::(); let sender = AppEventSender::new(tx); - let mut composer = - ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string()); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); - for ch in ['/', 'c'] { - let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE)); - } + type_chars_humanlike(&mut composer, &['/', 'c']); let (_result, _needs_redraw) = composer.handle_key_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE)); @@ -1671,12 +1685,15 @@ mod tests { let (tx, _rx) = unbounded_channel::(); let sender = AppEventSender::new(tx); - let mut composer = - ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string()); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); - for ch in ['/', 'm', 'e', 'n', 't', 'i', 'o', 'n'] { - let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE)); - } + type_chars_humanlike(&mut composer, &['/', 'm', 'e', 'n', 't', 'i', 'o', 'n']); let (result, _needs_redraw) = composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); @@ -1703,8 +1720,13 @@ mod tests { let (tx, _rx) = unbounded_channel::(); let sender = AppEventSender::new(tx); - let mut composer = - ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string()); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); // Define test cases: (paste content, is_large) let test_cases = [ @@ -1777,8 +1799,13 @@ mod tests { let (tx, _rx) = unbounded_channel::(); let sender = AppEventSender::new(tx); - let mut composer = - ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string()); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); // Define test cases: (content, is_large) let test_cases = [ @@ -1844,8 +1871,13 @@ mod tests { let (tx, _rx) = unbounded_channel::(); let sender = AppEventSender::new(tx); - let mut composer = - ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string()); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); // Define test cases: (cursor_position_from_end, expected_pending_count) let test_cases = [ @@ -1887,8 +1919,13 @@ mod tests { fn attach_image_and_submit_includes_image_paths() { let (tx, _rx) = unbounded_channel::(); let sender = AppEventSender::new(tx); - let mut composer = - ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string()); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); let path = PathBuf::from("/tmp/image1.png"); composer.attach_image(path.clone(), 32, 16, "PNG"); composer.handle_paste(" hi".into()); @@ -1906,8 +1943,13 @@ mod tests { fn attach_image_without_text_submits_empty_text_and_images() { let (tx, _rx) = unbounded_channel::(); let sender = AppEventSender::new(tx); - let mut composer = - ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string()); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); let path = PathBuf::from("/tmp/image2.png"); composer.attach_image(path.clone(), 10, 5, "PNG"); let (result, _) = @@ -1926,8 +1968,13 @@ mod tests { fn image_placeholder_backspace_behaves_like_text_placeholder() { let (tx, _rx) = unbounded_channel::(); let sender = AppEventSender::new(tx); - let mut composer = - ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string()); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); let path = PathBuf::from("/tmp/image3.png"); composer.attach_image(path.clone(), 20, 10, "PNG"); let placeholder = composer.attached_images[0].placeholder.clone(); @@ -1962,8 +2009,13 @@ mod tests { let (tx, _rx) = unbounded_channel::(); let sender = AppEventSender::new(tx); - let mut composer = - ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string()); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); // Insert an image placeholder at the start let path = PathBuf::from("/tmp/image_multibyte.png"); @@ -1983,8 +2035,13 @@ mod tests { fn deleting_one_of_duplicate_image_placeholders_removes_matching_entry() { let (tx, _rx) = unbounded_channel::(); let sender = AppEventSender::new(tx); - let mut composer = - ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string()); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); let path1 = PathBuf::from("/tmp/image_dup1.png"); let path2 = PathBuf::from("/tmp/image_dup2.png"); @@ -2025,8 +2082,13 @@ mod tests { let (tx, _rx) = unbounded_channel::(); let sender = AppEventSender::new(tx); - let mut composer = - ChatComposer::new(true, sender, false, "Ask Codex to do anything".to_string()); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); let needs_redraw = composer.handle_paste(tmp_path.to_string_lossy().to_string()); assert!(needs_redraw); @@ -2035,4 +2097,104 @@ mod tests { let imgs = composer.take_recent_submission_images(); assert_eq!(imgs, vec![tmp_path.clone()]); } + + #[test] + fn burst_paste_fast_small_buffers_and_flushes_on_stop() { + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + + let count = 32; + for _ in 0..count { + let _ = + composer.handle_key_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE)); + assert!( + composer.is_in_paste_burst(), + "expected active paste burst during fast typing" + ); + assert!( + composer.textarea.text().is_empty(), + "text should not appear during burst" + ); + } + + assert!( + composer.textarea.text().is_empty(), + "text should remain empty until flush" + ); + std::thread::sleep(ChatComposer::recommended_paste_flush_delay()); + let flushed = composer.flush_paste_burst_if_due(); + assert!(flushed, "expected buffered text to flush after stop"); + assert_eq!(composer.textarea.text(), "a".repeat(count)); + assert!( + composer.pending_pastes.is_empty(), + "no placeholder for small burst" + ); + } + + #[test] + fn burst_paste_fast_large_inserts_placeholder_on_flush() { + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + + let count = LARGE_PASTE_CHAR_THRESHOLD + 1; // > threshold to trigger placeholder + for _ in 0..count { + let _ = + composer.handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE)); + } + + // Nothing should appear until we stop and flush + assert!(composer.textarea.text().is_empty()); + std::thread::sleep(ChatComposer::recommended_paste_flush_delay()); + let flushed = composer.flush_paste_burst_if_due(); + assert!(flushed, "expected flush after stopping fast input"); + + let expected_placeholder = format!("[Pasted Content {count} chars]"); + assert_eq!(composer.textarea.text(), expected_placeholder); + assert_eq!(composer.pending_pastes.len(), 1); + assert_eq!(composer.pending_pastes[0].0, expected_placeholder); + assert_eq!(composer.pending_pastes[0].1.len(), count); + assert!(composer.pending_pastes[0].1.chars().all(|c| c == 'x')); + } + + #[test] + fn humanlike_typing_1000_chars_appears_live_no_placeholder() { + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + + let count = LARGE_PASTE_CHAR_THRESHOLD; // 1000 in current config + let chars: Vec = vec!['z'; count]; + type_chars_humanlike(&mut composer, &chars); + + assert_eq!(composer.textarea.text(), "z".repeat(count)); + assert!(composer.pending_pastes.is_empty()); + } } diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 949283f61a..c1e84beeef 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -13,6 +13,7 @@ use ratatui::layout::Constraint; use ratatui::layout::Layout; use ratatui::layout::Rect; use ratatui::widgets::WidgetRef; +use std::time::Duration; mod approval_modal_view; mod bottom_pane_view; @@ -21,6 +22,7 @@ mod chat_composer_history; mod command_popup; mod file_search_popup; mod list_selection_view; +mod paste_burst; mod popup_consts; mod scroll_state; mod selection_popup_common; @@ -69,6 +71,7 @@ pub(crate) struct BottomPaneParams { pub(crate) has_input_focus: bool, pub(crate) enhanced_keys_supported: bool, pub(crate) placeholder_text: String, + pub(crate) disable_paste_burst: bool, } impl BottomPane { @@ -81,6 +84,7 @@ impl BottomPane { params.app_event_tx.clone(), enhanced_keys_supported, params.placeholder_text, + params.disable_paste_burst, ), active_view: None, app_event_tx: params.app_event_tx, @@ -182,6 +186,9 @@ impl BottomPane { if needs_redraw { self.request_redraw(); } + if self.composer.is_in_paste_burst() { + self.request_redraw_in(ChatComposer::recommended_paste_flush_delay()); + } input_result } } @@ -382,12 +389,24 @@ impl BottomPane { self.frame_requester.schedule_frame(); } + pub(crate) fn request_redraw_in(&self, dur: Duration) { + self.frame_requester.schedule_frame_in(dur); + } + // --- History helpers --- pub(crate) fn set_history_metadata(&mut self, log_id: u64, entry_count: usize) { self.composer.set_history_metadata(log_id, entry_count); } + pub(crate) fn flush_paste_burst_if_due(&mut self) -> bool { + self.composer.flush_paste_burst_if_due() + } + + pub(crate) fn is_in_paste_burst(&self) -> bool { + self.composer.is_in_paste_burst() + } + pub(crate) fn on_history_entry_response( &mut self, log_id: u64, @@ -473,6 +492,7 @@ mod tests { has_input_focus: true, enhanced_keys_supported: false, placeholder_text: "Ask Codex to do anything".to_string(), + disable_paste_burst: false, }); pane.push_approval_request(exec_request()); assert_eq!(CancellationEvent::Handled, pane.on_ctrl_c()); @@ -492,6 +512,7 @@ mod tests { has_input_focus: true, enhanced_keys_supported: false, placeholder_text: "Ask Codex to do anything".to_string(), + disable_paste_burst: false, }); // Create an approval modal (active view). @@ -522,6 +543,7 @@ mod tests { has_input_focus: true, enhanced_keys_supported: false, placeholder_text: "Ask Codex to do anything".to_string(), + disable_paste_burst: false, }); // Start a running task so the status indicator is active above the composer. @@ -589,6 +611,7 @@ mod tests { has_input_focus: true, enhanced_keys_supported: false, placeholder_text: "Ask Codex to do anything".to_string(), + disable_paste_burst: false, }); // Begin a task: show initial status. @@ -619,6 +642,7 @@ mod tests { has_input_focus: true, enhanced_keys_supported: false, placeholder_text: "Ask Codex to do anything".to_string(), + disable_paste_burst: false, }); // Activate spinner (status view replaces composer) with no live ring. @@ -669,6 +693,7 @@ mod tests { has_input_focus: true, enhanced_keys_supported: false, placeholder_text: "Ask Codex to do anything".to_string(), + disable_paste_burst: false, }); pane.set_task_running(true); diff --git a/codex-rs/tui/src/bottom_pane/paste_burst.rs b/codex-rs/tui/src/bottom_pane/paste_burst.rs new file mode 100644 index 0000000000..b353d8677d --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/paste_burst.rs @@ -0,0 +1,246 @@ +use std::time::Duration; +use std::time::Instant; + +// Heuristic thresholds for detecting paste-like input bursts. +// Detect quickly to avoid showing typed prefix before paste is recognized +const PASTE_BURST_MIN_CHARS: u16 = 3; +const PASTE_BURST_CHAR_INTERVAL: Duration = Duration::from_millis(8); +const PASTE_ENTER_SUPPRESS_WINDOW: Duration = Duration::from_millis(120); + +#[derive(Default)] +pub(crate) struct PasteBurst { + last_plain_char_time: Option, + consecutive_plain_char_burst: u16, + burst_window_until: Option, + buffer: String, + active: bool, + // Hold first fast char briefly to avoid rendering flicker + pending_first_char: Option<(char, Instant)>, +} + +pub(crate) enum CharDecision { + /// Start buffering and retroactively capture some already-inserted chars. + BeginBuffer { retro_chars: u16 }, + /// We are currently buffering; append the current char into the buffer. + BufferAppend, + /// Do not insert/render this char yet; temporarily save the first fast + /// char while we wait to see if a paste-like burst follows. + RetainFirstChar, + /// Begin buffering using the previously saved first char (no retro grab needed). + BeginBufferFromPending, +} + +pub(crate) struct RetroGrab { + pub start_byte: usize, + pub grabbed: String, +} + +impl PasteBurst { + /// Recommended delay to wait between simulated keypresses (or before + /// scheduling a UI tick) so that a pending fast keystroke is flushed + /// out of the burst detector as normal typed input. + /// + /// Primarily used by tests and by the TUI to reliably cross the + /// paste-burst timing threshold. + pub fn recommended_flush_delay() -> Duration { + PASTE_BURST_CHAR_INTERVAL + Duration::from_millis(1) + } + + /// Entry point: decide how to treat a plain char with current timing. + pub fn on_plain_char(&mut self, ch: char, now: Instant) -> CharDecision { + match self.last_plain_char_time { + Some(prev) if now.duration_since(prev) <= PASTE_BURST_CHAR_INTERVAL => { + self.consecutive_plain_char_burst = + self.consecutive_plain_char_burst.saturating_add(1) + } + _ => self.consecutive_plain_char_burst = 1, + } + self.last_plain_char_time = Some(now); + + if self.active { + self.burst_window_until = Some(now + PASTE_ENTER_SUPPRESS_WINDOW); + return CharDecision::BufferAppend; + } + + // If we already held a first char and receive a second fast char, + // start buffering without retro-grabbing (we never rendered the first). + if let Some((held, held_at)) = self.pending_first_char + && now.duration_since(held_at) <= PASTE_BURST_CHAR_INTERVAL + { + self.active = true; + // take() to clear pending; we already captured the held char above + let _ = self.pending_first_char.take(); + self.buffer.push(held); + self.burst_window_until = Some(now + PASTE_ENTER_SUPPRESS_WINDOW); + return CharDecision::BeginBufferFromPending; + } + + if self.consecutive_plain_char_burst >= PASTE_BURST_MIN_CHARS { + return CharDecision::BeginBuffer { + retro_chars: self.consecutive_plain_char_burst.saturating_sub(1), + }; + } + + // Save the first fast char very briefly to see if a burst follows. + self.pending_first_char = Some((ch, now)); + CharDecision::RetainFirstChar + } + + /// Flush the buffered burst if the inter-key timeout has elapsed. + /// + /// Returns Some(String) when either: + /// - We were actively buffering paste-like input and the buffer is now + /// emitted as a single pasted string; or + /// - We had saved a single fast first-char with no subsequent burst and we + /// now emit that char as normal typed input. + /// + /// Returns None if the timeout has not elapsed or there is nothing to flush. + pub fn flush_if_due(&mut self, now: Instant) -> Option { + let timed_out = self + .last_plain_char_time + .is_some_and(|t| now.duration_since(t) > PASTE_BURST_CHAR_INTERVAL); + if timed_out && self.is_active_internal() { + self.active = false; + let out = std::mem::take(&mut self.buffer); + Some(out) + } else if timed_out { + // If we were saving a single fast char and no burst followed, + // flush it as normal typed input. + if let Some((ch, _at)) = self.pending_first_char.take() { + Some(ch.to_string()) + } else { + None + } + } else { + None + } + } + + /// While bursting: accumulate a newline into the buffer instead of + /// submitting the textarea. + /// + /// Returns true if a newline was appended (we are in a burst context), + /// false otherwise. + pub fn append_newline_if_active(&mut self, now: Instant) -> bool { + if self.is_active() { + self.buffer.push('\n'); + self.burst_window_until = Some(now + PASTE_ENTER_SUPPRESS_WINDOW); + true + } else { + false + } + } + + /// Decide if Enter should insert a newline (burst context) vs submit. + pub fn newline_should_insert_instead_of_submit(&self, now: Instant) -> bool { + let in_burst_window = self.burst_window_until.is_some_and(|until| now <= until); + self.is_active() || in_burst_window + } + + /// Keep the burst window alive. + pub fn extend_window(&mut self, now: Instant) { + self.burst_window_until = Some(now + PASTE_ENTER_SUPPRESS_WINDOW); + } + + /// Begin buffering with retroactively grabbed text. + pub fn begin_with_retro_grabbed(&mut self, grabbed: String, now: Instant) { + if !grabbed.is_empty() { + self.buffer.push_str(&grabbed); + } + self.active = true; + self.burst_window_until = Some(now + PASTE_ENTER_SUPPRESS_WINDOW); + } + + /// Append a char into the burst buffer. + pub fn append_char_to_buffer(&mut self, ch: char, now: Instant) { + self.buffer.push(ch); + self.burst_window_until = Some(now + PASTE_ENTER_SUPPRESS_WINDOW); + } + + /// Decide whether to begin buffering by retroactively capturing recent + /// chars from the slice before the cursor. + /// + /// Heuristic: if the retro-grabbed slice contains any whitespace or is + /// sufficiently long (>= 16 characters), treat it as paste-like to avoid + /// rendering the typed prefix momentarily before the paste is recognized. + /// This favors responsiveness and prevents flicker for typical pastes + /// (URLs, file paths, multiline text) while not triggering on short words. + /// + /// Returns Some(RetroGrab) with the start byte and grabbed text when we + /// decide to buffer retroactively; otherwise None. + pub fn decide_begin_buffer( + &mut self, + now: Instant, + before: &str, + retro_chars: usize, + ) -> Option { + let start_byte = retro_start_index(before, retro_chars); + let grabbed = before[start_byte..].to_string(); + let looks_pastey = + grabbed.chars().any(|c| c.is_whitespace()) || grabbed.chars().count() >= 16; + if looks_pastey { + // Note: caller is responsible for removing this slice from UI text. + self.begin_with_retro_grabbed(grabbed.clone(), now); + Some(RetroGrab { + start_byte, + grabbed, + }) + } else { + None + } + } + + /// Before applying modified/non-char input: flush buffered burst immediately. + pub fn flush_before_modified_input(&mut self) -> Option { + if self.is_active() { + self.active = false; + Some(std::mem::take(&mut self.buffer)) + } else { + None + } + } + + /// Clear only the timing window and any pending first-char. + /// + /// Does not emit or clear the buffered text itself; callers should have + /// already flushed (if needed) via one of the flush methods above. + pub fn clear_window_after_non_char(&mut self) { + self.consecutive_plain_char_burst = 0; + self.last_plain_char_time = None; + self.burst_window_until = None; + self.active = false; + self.pending_first_char = None; + } + + /// Returns true if we are in any paste-burst related transient state + /// (actively buffering, have a non-empty buffer, or have saved the first + /// fast char while waiting for a potential burst). + pub fn is_active(&self) -> bool { + self.is_active_internal() || self.pending_first_char.is_some() + } + + fn is_active_internal(&self) -> bool { + self.active || !self.buffer.is_empty() + } + + pub fn clear_after_explicit_paste(&mut self) { + self.last_plain_char_time = None; + self.consecutive_plain_char_burst = 0; + self.burst_window_until = None; + self.active = false; + self.buffer.clear(); + self.pending_first_char = None; + } +} + +pub(crate) fn retro_start_index(before: &str, retro_chars: usize) -> usize { + if retro_chars == 0 { + return before.len(); + } + before + .char_indices() + .rev() + .nth(retro_chars.saturating_sub(1)) + .map(|(idx, _)| idx) + .unwrap_or(0) +} diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index e687fc038f..5e1fd45fc6 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -604,6 +604,7 @@ impl ChatWidget { has_input_focus: true, enhanced_keys_supported, placeholder_text: placeholder, + disable_paste_burst: config.disable_paste_burst, }), active_exec_cell: None, config: config.clone(), @@ -652,6 +653,7 @@ impl ChatWidget { has_input_focus: true, enhanced_keys_supported, placeholder_text: placeholder, + disable_paste_burst: config.disable_paste_burst, }), active_exec_cell: None, config: config.clone(), @@ -858,6 +860,24 @@ impl ChatWidget { self.bottom_pane.handle_paste(text); } + // Returns true if caller should skip rendering this frame (a future frame is scheduled). + pub(crate) fn handle_paste_burst_tick(&mut self, frame_requester: FrameRequester) -> bool { + if self.bottom_pane.flush_paste_burst_if_due() { + // A paste just flushed; request an immediate redraw and skip this frame. + self.request_redraw(); + true + } else if self.bottom_pane.is_in_paste_burst() { + // While capturing a burst, schedule a follow-up tick and skip this frame + // to avoid redundant renders between ticks. + frame_requester.schedule_frame_in( + crate::bottom_pane::ChatComposer::recommended_paste_flush_delay(), + ); + true + } else { + false + } + } + fn flush_active_exec_cell(&mut self) { if let Some(active) = self.active_exec_cell.take() { self.last_history_was_exec = true; diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index edc6f7d030..758d28773a 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -164,6 +164,7 @@ fn make_chatwidget_manual() -> ( has_input_focus: true, enhanced_keys_supported: false, placeholder_text: "Ask Codex to do anything".to_string(), + disable_paste_burst: false, }); let widget = ChatWidget { app_event_tx, From c3a8b96a60d16eecf23985f1aeaf36d23796481d Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Thu, 28 Aug 2025 13:56:52 -0700 Subject: [PATCH 0382/1309] Add a VS Code Extension issue template (#2853) Template mostly copied from the bug template --- .../ISSUE_TEMPLATE/5-vs-code-extension.yml | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/5-vs-code-extension.yml diff --git a/.github/ISSUE_TEMPLATE/5-vs-code-extension.yml b/.github/ISSUE_TEMPLATE/5-vs-code-extension.yml new file mode 100644 index 0000000000..f2ba251a1d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/5-vs-code-extension.yml @@ -0,0 +1,50 @@ +name: 🧑‍💻 VS Code Extension +description: Report an issue with the VS Code extension +labels: + - extension + - needs triage +body: + - type: markdown + attributes: + value: | + Before submitting a new issue, please search for existing issues to see if your issue has already been reported. + If it has, please add a 👍 reaction (no need to leave a comment) to the existing issue instead of creating a new one. + + - type: input + id: version + attributes: + label: What version of the VS Code extension are you using? + - type: input + id: ide + attributes: + label: Which IDE are you using? + description: Like `VS Code`, `Cursor`, `Windsurf`, etc. + - type: input + id: platform + attributes: + label: What platform is your computer? + description: | + For MacOS and Linux: copy the output of `uname -mprs` + For Windows: copy the output of `"$([Environment]::OSVersion | ForEach-Object VersionString) $(if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" })"` in the PowerShell console + - type: textarea + id: steps + attributes: + label: What steps can reproduce the bug? + description: Explain the bug and provide a code snippet that can reproduce it. + validations: + required: true + - type: textarea + id: expected + attributes: + label: What is the expected behavior? + description: If possible, please provide text instead of a screenshot. + - type: textarea + id: actual + attributes: + label: What do you see instead? + description: If possible, please provide text instead of a screenshot. + - type: textarea + id: notes + attributes: + label: Additional information + description: Is there anything else you think we should know? From 6209d49520ee7b9f3262e44089c96ce69ca6e5cc Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 28 Aug 2025 14:21:10 -0700 Subject: [PATCH 0383/1309] Changed OAuth success screen to use the string "Codex" rather than "Codex CLI" (#2737) --- codex-rs/login/src/assets/success.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codex-rs/login/src/assets/success.html b/codex-rs/login/src/assets/success.html index eb2a0ee719..382f864c6a 100644 --- a/codex-rs/login/src/assets/success.html +++ b/codex-rs/login/src/assets/success.html @@ -2,7 +2,7 @@ - Sign into Codex CLI + Sign into Codex