## TL;DR
Fixes the issues when using Codex CLI with Zellij multiplexer. Before
this PR there would be no scrollback when using it inside a zellij
terminal.
## Problem
Addresses #2558
Zellij does not support ANSI scroll-region manipulation (`DECSTBM` /
Reverse Index) or the alternate screen buffer in the way traditional
terminals do. When codex's TUI runs inside Zellij, two things break: (1)
inline history insertion corrupts the display because the scroll-region
escape sequences are silently dropped or mishandled, and (2) the
composer textarea renders with inherited background/foreground styles
that produce unreadable text against Zellij's pane chrome.
## Mental model
The fix introduces a **Zellij mode** — a runtime boolean detected once
at startup via `codex_terminal_detection::terminal_info().is_zellij()` —
that gates two subsystems onto Zellij-safe terminal strategies:
- **History insertion** (`insert_history.rs`): Instead of using
`DECSTBM` scroll regions and Reverse Index (`ESC M`) to slide content
above the viewport, Zellij mode scrolls the screen by emitting `\n` at
the bottom row and then writes history lines at absolute positions. This
avoids every escape sequence Zellij mishandles.
- **Viewport expansion** (`tui.rs`): When the viewport grows taller than
available space, the standard path uses `scroll_region_up` on the
backend. Zellij mode instead emits newlines at the screen bottom to push
content up, then invalidates the ratatui diff buffer so the next draw is
a full repaint.
- **Composer rendering** (`chat_composer.rs`, `textarea.rs`): All text
rendering in the input area uses an explicit `base_style` with
`Color::Reset` foreground, preventing Zellij's pane styling from
bleeding into the textarea. The prompt chevron (`›`) and placeholder
text use explicit color constants instead of relying on `.bold()` /
`.dim()` modifiers that render inconsistently under Zellij.
## Non-goals
- This change does not fix or improve Zellij's terminal emulation
itself.
- It does not rearchitect the inline viewport model; it adds a parallel
code path gated on detection.
- It does not touch the alternate-screen disable logic (that already
existed and continues to use `is_zellij` via the same detection).
## Tradeoffs
- **Code duplication in `insert_history.rs`**: The Zellij and Standard
branches share the line-rendering loop (color setup, span merging,
`write_spans`) but differ in the scrolling preamble. The duplication is
intentional — merging them would force a complex conditional state
machine that's harder to reason about than two flat sequences.
- **`invalidate_viewport` after every Zellij history flush or viewport
expansion**: This forces a full repaint on every draw cycle in Zellij,
which is more expensive than ratatui's normal diff-based rendering. This
is necessary because Zellij's lack of scroll-region support means the
diff buffer's assumptions about what's on screen are invalid after we
manually move content.
- **Explicit colors vs semantic modifiers**: Replacing `.bold()` /
`.dim()` with `Color::Cyan` / `Color::DarkGray` / `Color::White` in the
Zellij branch sacrifices theme-awareness for correctness. If the project
ever adopts a theming system, Zellij styling will need to participate.
## Architecture
The Zellij detection flag flows through three layers:
1. **`codex_terminal_detection`** — `TerminalInfo::is_zellij()` (new
convenience method) reads the already-detected `Multiplexer` variant.
2. **`Tui` struct** — caches `is_zellij` at construction; passes it into
`update_inline_viewport`, `flush_pending_history_lines`, and
`insert_history_lines_with_mode`.
3. **`ChatComposer` struct** — independently caches `is_zellij` at
construction; uses it in `render_textarea` for style decisions.
The two caches (`Tui.is_zellij` and `ChatComposer.is_zellij`) are read
from the same global `OnceLock<TerminalInfo>`, so they always agree.
## Observability
No new logging, metrics, or tracing is introduced. Diagnosis depends on:
- Whether `ZELLIJ` or `ZELLIJ_SESSION_NAME` env vars are set (the
detection heuristic).
- Visual inspection of the rendered TUI inside Zellij vs a standard
terminal.
- The insta snapshot `zellij_empty_composer` captures the Zellij-mode
render path.
## Tests
- `terminal_info_reports_is_zellij` — unit test in `terminal-detection`
confirming the convenience method.
- `zellij_empty_composer_snapshot` — insta snapshot in `chat_composer`
validating the Zellij render path for an empty composer.
- `vt100_zellij_mode_inserts_history_and_updates_viewport` — integration
test in `insert_history` verifying that Zellij-mode history insertion
writes content and shifts the viewport.
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addresses #16560
Problem: `/status` stopped showing the source thread id in forked TUI
sessions after the app-server migration.
Solution: Carry fork source ids through app-server v2 thread data and
the TUI session adapter, and update TUI fixtures so `/status` matches
the old TUI behavior.
Recently, I merged a number of PRs to increase startup timeouts for
scripts that ran under PowerShell, but in the failure for
`suite::codex_tool::test_shell_command_approval_triggers_elicitation`, I
found this in the error logs when running on Bazel with BuildBuddy:
```
[mcp stderr] 2026-04-02T19:54:10.758951Z ERROR codex_core::tools::router: error=Exit code: 1
[mcp stderr] Wall time: 0.2 seconds
[mcp stderr] Output:
[mcp stderr] 'New-Item' is not recognized as an internal or external command,
[mcp stderr] operable program or batch file.
[mcp stderr]
```
This error implies that the command was run under `cmd.exe` instead of
`pwsh.exe`. Under GitHub Actions, I suspect that the `%PATH%` that is
passed to our Bazel builder is scrubbed such that our tests cannot find
PowerShell where GitHub installs it. Having these explicit fallback
paths should help.
While we could enable these only for tests, I don't see any harm in
keeping them in production, as well.
Addresses #16389
Problem: `/review` follow-ups can crash when app-server TUI steers with
a stale active turn id; #14717 introduced the client-side race, and
#15714 only handled the “no active turn” half.
Solution: Treat turn-id mismatch as stale cached state too, sync to the
server’s current turn id, retry once, and let review turns fall into the
existing queue path.
Addresses #16562
Problem: Resume picker could keep a stale backend-provided thread title
instead of the latest name from session_index.jsonl.
Solution: Always backfill/override picker row names from local
session_index.jsonl and cover stale-name replacement with a regression
test.
Addresses #16514
Problem: Resume picker could show “No sessions yet” before the initial
session fetch finished.
Solution: Render a loading message while the first page is pending, and
keep the empty state for truly empty results.
In https://github.com/openai/codex/pull/16528, I am trying to get tests
running under Bazel on Windows, but currently I see:
```
thread 'suite::user_shell_cmd::user_shell_command_does_not_set_network_sandbox_env_var' (10220) panicked at core/tests\suite\user_shell_cmd.rs:358:5:
assertion failed: `(left == right)`
Diff < left / right > :
<1
>0
```
This PR updates the `assert_eq!()` to provide more information to help
diagnose the failure.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/16606).
* #16608
* __->__ #16606
## Why
`external_bearer_only_auth_manager_uses_cached_provider_token` can fail
on Windows when cold `powershell.exe` startup exceeds the provider-auth
helper's 1s timeout. When that happens,
`AuthManager::resolve_external_api_key_auth()` [logs the resolver error
and returns
`None`](https://github.com/openai/codex/blob/024b08b411fe/codex-rs/login/src/auth/manager.rs#L1449-L1455),
which is exactly the assertion failure from the flake.
## What
- Invoke `powershell.exe` explicitly in the Windows provider-auth test
helpers in `login/src/auth/auth_tests.rs`.
- Increase the helper timeout to `10_000` ms and document why that slack
exists.
## Verification
- `cargo test -p codex-login`
Running multiple builds with no changes causes some differences, we see
that
https://app.buildbuddy.io/compare/a9719629-1660-4735-a477-d66357f234fb...df85310b-eb5c-4c10-8b79-4d0449ba6cdd#file
shows the file-differences between two Bazel builds.
These differences are caused by a non-deterministic `.git` entry in the
rules_rs crates that are created with `crate_git_repository`.
As a way to make these deterministic, we can remove this entry after we
download the git source, so that the input to the compile action is
deterministic.
### CLA
I have read the CLA Document and I hereby sign the CLA
## Why
`codex-rs/exec/src/lib.rs` already keeps unit tests in a sibling
`lib_tests.rs` module so the implementation stays top-heavy and easier
to read. This applies that same layout to the rest of
`codex-rs/exec/src` so each production file keeps its entry points and
helpers ahead of test code.
## What
- Move inline unit tests out of `cli.rs`, `main.rs`,
`event_processor_with_human_output.rs`, and
`event_processor_with_jsonl_output.rs` into sibling `*_tests.rs` files.
- Keep test modules wired through `#[cfg(test)]` plus `#[path = "..."]
mod tests;`, matching the `lib.rs` pattern.
- Preserve the existing test coverage and assertions while making this a
source-layout-only refactor.
## Verification
- `cargo test -p codex-exec`
## Why
The main Bazel CI lanes need compact execution logs to investigate cache
misses and unexpected rebuilds, but local users of the shared wrapper
should not pay that log-generation cost by default.
## What Changed
-
[`.github/scripts/run-bazel-ci.sh`](https://github.com/openai/codex/blob/a6ec239a24fd21507bb75a28f17277653f52e3a2/.github/scripts/run-bazel-ci.sh#L149-L153)
now appends `--execution_log_compact_file=...` only when
`CODEX_BAZEL_EXECUTION_LOG_COMPACT_DIR` is set; the caller owns creating
that directory.
-
[`.github/workflows/bazel.yml`](https://github.com/openai/codex/blob/a6ec239a24fd21507bb75a28f17277653f52e3a2/.github/workflows/bazel.yml#L66-L174)
enables that env var only for the main `test` and `clippy` jobs, creates
the temp log directory in each job, and uploads the resulting `*.zst`
files from `runner.temp`.
## Verification
- `bash -n .github/scripts/run-bazel-ci.sh`
- Parsed `.github/workflows/bazel.yml` as YAML.
- Ran a local opt-in wrapper smoke test and confirmed it writes
`execution-log-cquery-local-*.zst` when the caller pre-creates
`CODEX_BAZEL_EXECUTION_LOG_COMPACT_DIR`.
## Why
This finishes the config-type move out of `codex-core` by removing the
temporary compatibility shim in `codex_core::config::types`. Callers now
depend on `codex-config` directly, which keeps these config model types
owned by the config crate instead of re-expanding `codex-core` as a
transitive API surface.
## What Changed
- Removed the `codex-rs/core/src/config/types.rs` re-export shim and the
`core::config::ApprovalsReviewer` re-export.
- Updated `codex-core`, `codex-cli`, `codex-tui`, `codex-app-server`,
`codex-mcp-server`, and `codex-linux-sandbox` call sites to import
`codex_config::types` directly.
- Added explicit `codex-config` dependencies to downstream crates that
previously relied on the `codex-core` re-export.
- Regenerated `codex-rs/core/config.schema.json` after updating the
config docs path reference.
## Why
`codex-rs/core/src/config/types.rs` is a plain config-type module with
no dependency on `codex-core`. Moving it into `codex-config` shrinks the
core crate and gives config-only consumers a more natural dependency
boundary.
## What Changed
- Added `codex_config::types` with the moved structs, enums, constants,
and unit tests.
- Kept `codex_core::config::types` as a compatibility re-export to avoid
a broad call-site migration in this PR.
- Switched notice-table writes in `core/src/config/edit.rs` to a local
`NOTICE_TABLE_KEY` constant.
- Added the `wildmatch` runtime dependency and `tempfile` test
dependency to `codex-config`.
## Why
#16513 moved pure tool-registry planning into `codex-tools`, but much of
the corresponding spec/feature-gating coverage still lived in
`codex-core`. That leaves the tests for planner behavior in the crate
that no longer owns that logic and makes the next extraction steps
harder to review.
## What
Move the planner-only `spec_tests.rs` coverage into
`codex-rs/tools/src/tool_registry_plan_tests.rs` and wire it up from
`codex-rs/tools/src/tool_registry_plan.rs` using the crate-local `#[path
= "tool_registry_plan_tests.rs"] mod tests;` pattern.
The `codex-core` test file now keeps the core-side integration checks:
router-visible model tool lists, namespaced handler alias registration,
shell adapter behavior, and MCP schema edge cases that still exercise
the `core` binding layer.
## Verification
- `cargo test -p codex-tools`
- `cargo test -p codex-core tools::spec::tests`
## Why
This is a larger step in the `codex-core` -> `codex-tools` migration
called out in `AGENTS.md`.
`codex-rs/core/src/tools/spec.rs` had become mostly pure tool-spec
assembly plus handler registration. That made it hard to move more of
the tool-definition layer into `codex-tools`, because the runtime
binding and the crate-independent planning logic were still interleaved
in one function.
Splitting those concerns gives `codex-tools` ownership of the
declarative registry plan while keeping `codex-core` responsible for
instantiating concrete handlers.
## What Changed
- Add a `codex-tools` registry-plan layer in
`codex-rs/tools/src/tool_registry_plan.rs` and
`codex-rs/tools/src/tool_registry_plan_types.rs`.
- Move feature-gated tool-spec assembly, MCP/dynamic tool conversion,
tool-search aliases, and code-mode nested-plan expansion into
`codex-tools`.
- Keep `codex-rs/core/src/tools/spec.rs` as the core-side adapter that
maps each planned handler kind to concrete runtime handler instances.
- Update `spec_tests.rs` to import the moved `codex_tools` symbols
directly instead of relying on top-level `spec.rs` re-exports.
This is intended to be a straight refactor with no behavior change and
no new test surface.
## Verification
- `cargo test -p codex-tools`
- `cargo test -p codex-core tools::spec::tests`
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/16513).
* #16521
* __->__ #16513
This PR updates `Cargo.lock` to remove `codex-core` from
`mcp_test_support`, which corresponds to
`codex-rs/mcp-server/tests/common/Cargo.toml`. As noted in #16512, it
updated that crate to drop its `codex-core` dependency.
## Why
`codex-core` was re-exporting APIs owned by sibling `codex-*` crates,
which made downstream crates depend on `codex-core` as a proxy module
instead of the actual owner crate.
Removing those forwards makes crate boundaries explicit and lets leaf
crates drop unnecessary `codex-core` dependencies. In this PR, this
reduces the dependency on `codex-core` to `codex-login` in the following
files:
```
codex-rs/backend-client/Cargo.toml
codex-rs/mcp-server/tests/common/Cargo.toml
```
## What
- Remove `codex-rs/core/src/lib.rs` re-exports for symbols owned by
`codex-login`, `codex-mcp`, `codex-rollout`, `codex-analytics`,
`codex-protocol`, `codex-shell-command`, `codex-sandboxing`,
`codex-tools`, and `codex-utils-path`.
- Delete the `default_client` forwarding shim in `codex-rs/core`.
- Update in-crate and downstream callsites to import directly from the
owning `codex-*` crate.
- Add direct Cargo dependencies where callsites now target the owner
crate, and remove `codex-core` from `codex-rs/backend-client`.
## Why
This is another small step in the `codex-core` -> `codex-tools`
migration described in `AGENTS.md`.
`core/src/tools/spec.rs` and `core/src/tools/code_mode/mod.rs` were both
hand-rolling the same pure transformation: convert visible `ToolSpec`s
into code-mode nested tool definitions, then sort and deduplicate by
tool name. That logic does not depend on core runtime state or handlers,
so keeping it in `codex-core` makes `spec.rs` harder to peel out later
than it needs to be.
## What Changed
- Add `collect_code_mode_tool_definitions()` to
`codex-rs/tools/src/code_mode.rs`.
- Reuse that helper from `codex-rs/core/src/tools/spec.rs` when
assembling the `exec` tool description.
- Reuse the same helper from `codex-rs/core/src/tools/code_mode/mod.rs`
when exposing nested tool metadata to the code-mode runtime.
This is intended to be a straight refactor with no behavior change and
no new test surface.
## Verification
- `cargo test -p codex-tools`
- `cargo test -p codex-core tools::spec::tests`
- `cargo test -p codex-core code_mode_only_`
## Why
`codex-mcp` already owns the shared MCP API surface, including `auth`,
`McpConfig`, `CODEX_APPS_MCP_SERVER_NAME`, and tool-name helpers in
[`codex-rs/codex-mcp/src/mcp/mod.rs`](https://github.com/openai/codex/blob/f61e85dbfb5373cde6827d232ac8ea447c237e81/codex-rs/codex-mcp/src/mcp/mod.rs#L1-L35).
Re-exporting that surface from `codex_core::mcp` gives downstream crates
two import paths for the same API and hides the real crate dependency.
This PR keeps `codex_core::mcp` focused on the local `McpManager`
wrapper in
[`codex-rs/core/src/mcp.rs`](https://github.com/openai/codex/blob/f61e85dbfb5373cde6827d232ac8ea447c237e81/codex-rs/core/src/mcp.rs#L13-L40)
and makes consumers import shared MCP APIs from `codex_mcp` directly.
## What
- Remove the `codex_mcp::mcp` re-export surface from `core/src/mcp.rs`.
- Update `codex-core` internals plus `codex-app-server`, `codex-cli`,
and `codex-tui` test code to import MCP APIs from `codex_mcp::mcp`
directly.
- Add explicit `codex-mcp` dependencies where those crates now use that
API surface, and refresh `Cargo.lock`.
## Verification
- `just bazel-lock-check`
- `cargo test -p codex-core -p codex-cli -p codex-tui`
- `codex-cli` passed.
- `codex-core` still fails five unrelated config tests in
`core/src/config/config_tests.rs` (`approvals_reviewer_*` and
`smart_approvals_alias_*`).
- A broader `cargo test -p codex-core -p codex-app-server -p codex-cli
-p codex-tui` run previously hung in `codex-app-server` test
`in_process_start_uses_requested_session_source_for_thread_start`.
## Why
This is another incremental step in the `codex-core` -> `codex-tools`
migration called out in `AGENTS.md`: keep pure tool-definition and
wire-shaping logic out of `codex-core` so the core crate can stay
focused on runtime orchestration.
`request_user_input` already had its spec and mode-availability helpers
in `codex-tools` after #16471. The remaining argument validation and
normalization still lived in the core runtime handler, which left that
tool split across the two crates.
## What Changed
- Export `REQUEST_USER_INPUT_TOOL_NAME` and
`normalize_request_user_input_args()` from
`codex-rs/tools/src/request_user_input_tool.rs`.
- Use that `codex-tools` surface from `codex-rs/core/src/tools/spec.rs`
and `codex-rs/core/src/tools/handlers/request_user_input.rs`.
- Keep the core handler responsible for payload parsing, session
dispatch, cancellation handling, and response serialization.
This is intended to be a straight refactor with no behavior change.
## Verification
- `cargo test -p codex-tools`
- `cargo test -p codex-core request_user_input`