Commit Graph

1186 Commits

  • feat: include available decisions in command approval requests (#12758)
    Command-approval clients currently infer which choices to show from
    side-channel fields like `networkApprovalContext`,
    `proposedExecpolicyAmendment`, and `additionalPermissions`. That makes
    the request shape harder to evolve, and it forces each client to
    replicate the server's heuristics instead of receiving the exact
    decision list for the prompt.
    
    This PR introduces a mapping between `CommandExecutionApprovalDecision`
    and `codex_protocol::protocol::ReviewDecision`:
    
    ```rust
    impl From<CoreReviewDecision> for CommandExecutionApprovalDecision {
        fn from(value: CoreReviewDecision) -> Self {
            match value {
                CoreReviewDecision::Approved => Self::Accept,
                CoreReviewDecision::ApprovedExecpolicyAmendment {
                    proposed_execpolicy_amendment,
                } => Self::AcceptWithExecpolicyAmendment {
                    execpolicy_amendment: proposed_execpolicy_amendment.into(),
                },
                CoreReviewDecision::ApprovedForSession => Self::AcceptForSession,
                CoreReviewDecision::NetworkPolicyAmendment {
                    network_policy_amendment,
                } => Self::ApplyNetworkPolicyAmendment {
                    network_policy_amendment: network_policy_amendment.into(),
                },
                CoreReviewDecision::Abort => Self::Cancel,
                CoreReviewDecision::Denied => Self::Decline,
            }
        }
    }
    ```
    
    And updates `CommandExecutionRequestApprovalParams` to have a new field:
    
    ```rust
    available_decisions: Option<Vec<CommandExecutionApprovalDecision>>
    ```
    
    when, if specified, should make it easier for clients to display an
    appropriate list of options in the UI.
    
    This makes it possible for `CoreShellActionProvider::prompt()` in
    `unix_escalation.rs` to specify the `Vec<ReviewDecision>` directly,
    adding support for `ApprovedForSession` when approving a skill script,
    which was previously missing in the TUI.
    
    Note this results in a significant change to `exec_options()` in
    `approval_overlay.rs`, as the displayed options are now derived from
    `available_decisions: &[ReviewDecision]`.
    
    ## What Changed
    
    - Add `available_decisions` to
    [`ExecApprovalRequestEvent`](https://github.com/openai/codex/blob/de00e932dd9801de0a4faac0519162099753f331/codex-rs/protocol/src/approvals.rs#L111-L175),
    including helpers to derive the legacy default choices when older
    senders omit the field.
    - Map `codex_protocol::protocol::ReviewDecision` to app-server
    `CommandExecutionApprovalDecision` and expose the ordered list as
    experimental `availableDecisions` in
    [`CommandExecutionRequestApprovalParams`](https://github.com/openai/codex/blob/de00e932dd9801de0a4faac0519162099753f331/codex-rs/app-server-protocol/src/protocol/v2.rs#L3798-L3807).
    - Thread optional `available_decisions` through the core approval path
    so Unix shell escalation can explicitly request `ApprovedForSession` for
    session-scoped approvals instead of relying on client heuristics.
    [`unix_escalation.rs`](https://github.com/openai/codex/blob/de00e932dd9801de0a4faac0519162099753f331/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs#L194-L214)
    - Update the TUI approval overlay to build its buttons from the ordered
    decision list, while preserving the legacy fallback when
    `available_decisions` is missing.
    - Update the app-server README, test client output, and generated schema
    artifacts to document and surface the new field.
    
    ## Testing
    
    - Add `approval_overlay.rs` coverage for explicit decision lists,
    including the generic `ApprovedForSession` path and network approval
    options.
    - Update `chatwidget/tests.rs` and app-server protocol tests to populate
    the new optional field and keep older event shapes working.
    
    ## Developers Docs
    
    - If we document `item/commandExecution/requestApproval` on
    [developers.openai.com/codex](https://developers.openai.com/codex), add
    experimental `availableDecisions` as the preferred source of approval
    choices and note that older servers may omit it.
  • Revert "Add skill approval event/response (#12633)" (#12811)
    This reverts commit https://github.com/openai/codex/pull/12633. We no
    longer need this PR, because we favor sending normal exec command
    approval server request with `additional_permissions` of skill
    permissions instead
  • Remove steer feature flag (#12026)
    All code should go in the direction that steer is enabled
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Enable request_user_input in Default mode (#12735)
    ## Summary
    - allow `request_user_input` in Default collaboration mode as well as
    Plan
    - update the Default-mode instructions to prefer assumptions first and
    use `request_user_input` only when a question is unavoidable
    - update request_user_input and app-server tests to match the new
    Default-mode behavior
    - refactor collaboration-mode availability plumbing into
    `CollaborationModesConfig` for future mode-related flags
    
    ## Codex author
    `codex resume 019c9124-ed28-7c13-96c6-b916b1c97d49`
  • make 5.3-codex visible in cli for api users (#12808)
    5.3-codex released in api, mark it visible for API users via bundled
    `models.json`.
  • Update Codex docs success link (#12805)
    Fix a stale documentation link in the sign-in flow
  • feat(app-server): add ThreadItem::DynamicToolCall (#12732)
    Previously, clients would call `thread/start` with dynamic_tools set,
    and when a model invokes a dynamic tool, it would just make the
    server->client `item/tool/call` request and wait for the client's
    response to complete the tool call. This works, but it doesn't have an
    `item/started` or `item/completed` event.
    
    Now we are doing this:
    - [new] emit `item/started` with `DynamicToolCall` populated with the
    call arguments
    - send an `item/tool/call` server request
    - [new] once the client responds, emit `item/completed` with
    `DynamicToolCall` populated with the response.
    
    Also, with `persistExtendedHistory: true`, dynamic tool calls are now
    reconstructable in `thread/read` and `thread/resume` as
    `ThreadItem::DynamicToolCall`.
  • Promote js_repl to experimental with Node requirement (#12712)
    ## Summary
    
    - Promote `js_repl` to an experimental feature that users can enable
    from `/experimental`.
    - Add `js_repl` experimental metadata, including the Node prerequisite
    and activation guidance.
    - Add regression coverage for the feature metadata and the
    `/experimental` popup.
    
    ## What Changed
    
    - Changed `Feature::JsRepl` from `Stage::UnderDevelopment` to
    `Stage::Experimental`.
    - Added experimental metadata for `js_repl` in `core/src/features.rs`:
      - name: `JavaScript REPL`
    - description: calls out interactive website debugging, inline
    JavaScript execution, and the required Node version (`>= v24.13.1`)
    - announcement: tells users to enable it, then start a new chat or
    restart Codex
    - Added a core unit test that verifies:
      - `js_repl` is experimental
      - `js_repl` is disabled by default
    - the hardcoded Node version in the description matches
    `node-version.txt`
    - Added a TUI test that opens the `/experimental` popup and verifies the
    rendered `js_repl` entry includes the Node requirement text.
    
    ## Testing
    
    - `just fmt`
    - `cargo test -p codex-tui`
    - `cargo test -p codex-core` (unit-test phase passed; stopped during the
    long `tests/all.rs` integration suite)
  • Display pending child-thread approvals in TUI (#12767)
    Summary
    - propagate approval policy from parent to spawned agents and drop the
    Never override so sub-agents respect the caller’s request
    - refresh the pending-approval list whenever events arrive or the active
    thread changes and surface the list above the composer for inactive
    threads
    - add widgets, helpers, and tests covering the new pending-thread
    approval UI state
    
    ![Uploading Screenshot 2026-02-25 at 11.02.18.png…]()
  • feat: add search term to thread list (#12578)
    Add `searchTerm` to `thread/list` that will search for a match in the
    titles (the condition being `searchTerm` $$\in$$ `title`)
  • Surface skill permission profiles in zsh-fork exec approvals (#12753)
    ## Summary
    
    - Preserve each skill’s raw permissions block as a permission_profile on
    SkillMetadata during skill loading.
    - Keep compiling that same metadata into the existing runtime
    Permissions object, so current enforcement
        behavior stays intact.
    - When zsh-fork intercepts execution of a script that belongs to a
    skill, include the skill’s
        permission_profile in the exec approval request.
    - This lets approval UIs show the extra filesystem access the skill
    declared when prompting for approval.
  • feat(ui): add network approval persistence plumbing (#12358)
    ## Summary
    - add TUI approval options for persistent network host rules
    - add app-server v2 approval payload plumbing for network approval
    context + proposed network policy amendments
    - add app-server handling to translate `applyNetworkPolicyAmendment`
    decisions back into core review decisions
    - update docs/test client output and generated app-server schemas/types
  • fix: chatwidget was not honoring approval_id for an ExecApprovalRequestEvent (#12746)
    ## Why
    
    `ExecApprovalRequestEvent` can carry a distinct `approval_id` for
    subcommand approvals, including the `execve`-intercepted zsh-fork path.
    
    The session registers the pending approval callback under `approval_id`
    when one is present, but `ChatWidget` was stashing `call_id` in the
    approval modal state. When the user approved the command in the TUI, the
    response was sent back with the wrong identifier, so the pending
    approval could not be matched and the approval callback would not
    resolve.
    
    Note `approval_id` was introduced in
    https://github.com/openai/codex/pull/12051.
    
    ## What changed
    
    - In `tui/src/chatwidget.rs`, `ChatWidget` now uses
    `ExecApprovalRequestEvent::effective_approval_id()` when constructing
    `ApprovalRequest::Exec`.
    - That preserves the existing behavior for normal shell and
    `unified_exec` approvals, where `approval_id` is absent and the
    effective id still falls back to `call_id`.
    - For subcommand approvals that provide a distinct `approval_id`, the
    TUI now sends back the same key that
    `Session::request_command_approval()` registered.
    
    ## Verification
    
    - Traced the approval flow end to end to confirm the same effective
    approval id is now used on both sides of the round trip:
    - `Session::request_command_approval()` registers the pending callback
    under `approval_id.unwrap_or(call_id)`.
    - `ChatWidget` now emits `Op::ExecApproval` with that same effective id.
  • chore: migrate additional permissions to PermissionProfile (#12731)
    This PR replaces the old `additional_permissions.fs_read/fs_write` shape
    with a shared `PermissionProfile`
    model and wires it through the command approval, sandboxing, protocol,
    and TUI layers. The schema is adopted from the
    `SkillManifestPermissions`, which is also refactored to use this unified
    struct. This helps us easily expose permission profiles in app
    server/core as a follow-up.
  • feat: pass helper executable paths via Arg0DispatchPaths (#12719)
    ## Why
    
    `codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs` previously
    located `codex-execve-wrapper` by scanning `PATH` and sibling
    directories. That lookup is brittle and can select the wrong binary when
    the runtime environment differs from startup assumptions.
    
    We already pass `codex-linux-sandbox` from `codex-arg0`;
    `codex-execve-wrapper` should use the same startup-driven path plumbing.
    
    ## What changed
    
    - Introduced `Arg0DispatchPaths` in `codex-arg0` to carry both helper
    executable paths:
      - `codex_linux_sandbox_exe`
      - `main_execve_wrapper_exe`
    - Updated `arg0_dispatch_or_else()` to pass `Arg0DispatchPaths` to
    top-level binaries and preserve helper paths created in
    `prepend_path_entry_for_codex_aliases()`.
    - Threaded `Arg0DispatchPaths` through entrypoints in `cli`, `exec`,
    `tui`, `app-server`, and `mcp-server`.
    - Added `main_execve_wrapper_exe` to core configuration plumbing
    (`Config`, `ConfigOverrides`, and `SessionServices`).
    - Updated zsh-fork shell escalation to consume the configured
    `main_execve_wrapper_exe` and removed path-sniffing fallback logic.
    - Updated app-server config reload paths so reloaded configs keep the
    same startup-provided helper executable paths.
    
    ## References
    
    - [`Arg0DispatchPaths`
    definition](https://github.com/openai/codex/blob/e355b43d5c2a771f045296a6deae10d7c9c36ec6/codex-rs/arg0/src/lib.rs#L20-L24)
    - [`arg0_dispatch_or_else()` forwarding both
    paths](https://github.com/openai/codex/blob/e355b43d5c2a771f045296a6deae10d7c9c36ec6/codex-rs/arg0/src/lib.rs#L145-L176)
    - [zsh-fork escalation using configured wrapper
    path](https://github.com/openai/codex/blob/e355b43d5c2a771f045296a6deae10d7c9c36ec6/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs#L109-L150)
    
    ## Testing
    
    - `cargo check -p codex-arg0 -p codex-core -p codex-exec -p codex-tui -p
    codex-mcp-server -p codex-app-server`
    - `cargo test -p codex-arg0`
    - `cargo test -p codex-core tools::runtimes::shell::unix_escalation:: --
    --nocapture`
  • fix: clarify the value of SkillMetadata.path (#12729)
    Rename `SkillMetadata.path` to `SkillMetadata.path_to_skills_md` for
    clarity.
    
    Would ideally change the type to `AbsolutePathBuf`, but that can be done
    later.
  • feat(tui) - /copy (#12613)
    # /copy!
    
    /copy allows you to copy the latest **complete** message from Codex on
    the TUI.
  • Add TUI realtime conversation mode (#12687)
    - Add a hidden `realtime_conversation` feature flag and `/realtime`
    slash command for start/stop live voice sessions.
    - Reuse transcription composer/footer UI for live metering, stream mic
    audio, play assistant audio, render realtime user text events, and
    force-close on feature disable.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • feat(tui): add theme-aware diff backgrounds with capability-graded palettes (#12581)
    ## Problem
    
    Diff lines used only foreground colors (green/red) with no background
    tinting, making them hard to scan. The gutter (line numbers) also had no
    theme awareness — dimmed text was fine on dark terminals but unreadable
    on light ones.
    
    ## Mental model
    
    Each diff line now has four styled layers: **gutter** (line number),
    **sign** (`+`/`-`), **content** (text), and **line background** (full
    terminal width). A `DiffTheme` enum (`Dark` / `Light`) is selected once
    per render by probing the terminal's queried background via
    `default_bg()`. A companion `DiffColorLevel` enum (`TrueColor` /
    `Ansi256` / `Ansi16`) is derived from `stdout_color_level()` and gates
    which palette is used. All style helpers dispatch on `(theme,
    DiffLineType, color_level)` to pick the right colors.
    
    | Theme Picker Wide | Theme Picker Narrow |
    |---|---|
    | <img width="1552" height="1012" alt="image"
    src="https://github.com/user-attachments/assets/231b21b7-32d4-4727-80ed-7d01924954be"
    /> | <img width="795" height="1012" alt="image"
    src="https://github.com/user-attachments/assets/549cacdf-daec-43c9-ad64-2a28d16d140e"
    /> |
    
    | Dark BG - 16 colors | Dark BG - 256 colors | Dark BG - True Colors |
    |---|---|---|
    | <img width="1552" height="1012" alt="dark-16colors"
    src="https://github.com/user-attachments/assets/fba36de3-c101-47d4-9e63-88cdd00410d0"
    /> | <img width="1552" height="1012" alt="dark-256colors"
    src="https://github.com/user-attachments/assets/f39e4307-c6b0-49c4-b4fe-bd26d3d8e41c"
    /> | <img width="1552" height="1012" alt="dark-truecolor"
    src="https://github.com/user-attachments/assets/1af4ec57-04bf-4dfb-8a44-0ab5e5aaaf18"
    /> |
    
    | Light BG - 16 colors | Light BG - 256 colors | Light BG - True Colors
    |
    |---|---|---|
    | <img width="1552" height="1012" alt="light-16colors"
    src="https://github.com/user-attachments/assets/2b5423d1-74b4-4b1e-8123-7c2488ff436b"
    /> | <img width="1552" height="1012" alt="light-256colors"
    src="https://github.com/user-attachments/assets/c94cff9a-8d3e-42c9-bbe7-079da39953a8"
    /> | <img width="1552" height="1012" alt="light-truecolor"
    src="https://github.com/user-attachments/assets/f73da626-725f-4452-99ee-69ef706df2c6"
    /> |
    
    ## Non-goals
    
    - No runtime theme switching beyond what `default_bg()` already
    provides.
    - No change to syntax highlighting theme selection or the highlight
    module.
    
    ## Tradeoffs
    
    - Three fixed palettes (truecolor RGB, 256-color indexed, 16-color
    named) are maintained rather than using `best_color` nearest-match. This
    is deliberate: `supports_color::on_cached(Stream::Stdout)` can misreport
    capabilities once crossterm enters the alternate screen, so hand-picked
    palette entries give better visual results than automatic quantization.
    - Delete lines in the syntax-highlighted path get `Modifier::DIM` to
    visually recede compared to insert lines. This trades some readability
    of deleted code for scan-ability of additions.
    - The theme picker's diff preview sets `preserve_side_content_bg: true`
    on `ListSelectionView` so diff background tints survive into the side
    panel. Other popups keep the default (`false`) to preserve their
    reset-background look.
    
    ## Architecture
    
    - **Color constants** are module-level `const` items grouped by palette
    tier: `DARK_TC_*` / `LIGHT_TC_*` (truecolor RGB tuples), `DARK_256_*` /
    `LIGHT_256_*` (xterm indexed), with named `Color` variants used for the
    16-color tier.
    - **`DiffTheme`** is a private enum; `diff_theme()` probes the terminal
    and `diff_theme_for_bg()` is the testable pure-function version.
    - **`DiffColorLevel`** is a private enum derived from `StdoutColorLevel`
    via `diff_color_level()`.
    - **Palette helpers** (`add_line_bg`, `del_line_bg`, `light_gutter_fg`,
    `light_add_num_bg`, `light_del_num_bg`) each take `(DiffTheme,
    DiffColorLevel)` or just `DiffColorLevel` and return a `Color`.
    - **Style helpers** (`style_line_bg_for`, `style_gutter_for`,
    `style_sign_add`, `style_sign_del`, `style_add`, `style_del`) each take
    `(DiffLineType, DiffTheme, DiffColorLevel)` or `(DiffTheme,
    DiffColorLevel)` and return a `Style`.
    - **`push_wrapped_diff_line_inner_with_theme_and_color_level`** is the
    innermost renderer, accepting both theme and color level so tests can
    exercise any combination without depending on the terminal.
    - **Line-level background** is applied via
    `RtLine::from(...).style(line_bg)` so the tint extends across the full
    terminal width, not just the text content.
    - **Theme picker integration**: `ListSelectionView` gained a
    `preserve_side_content_bg` flag. When `true`, the side panel skips
    `force_bg_to_terminal_bg`, letting diff preview backgrounds render
    faithfully.
    
    ## Observability
    
    No new logging. Theme selection is deterministic from `default_bg()`,
    which is already queried and cached at TUI startup.
    
    ## Tests
    
    1. **`DiffTheme` is determined per `render_change` call** — if
    `default_bg()` changes mid-render (e.g. `requery_default_colors()`
    fires), different file chunks could render with different themes. Low
    risk in practice since re-query only happens on explicit user action.
    2. **16-color tier uses named `Color` variants** (`Color::Green`,
    `Color::Red`, etc.) which the terminal maps to its own palette. On
    unusual terminal themes these could clash with the background.
    Acceptable since 16-color terminals already have unpredictable color
    rendering.
    3. **Light-theme `style_add` / `style_del` set bg but no fg** — on light
    terminals, non-syntax-highlighted content uses the terminal's default
    foreground against a pastel background. If the terminal's default fg
    happens to be very light, contrast could suffer. This is an edge case
    since light-terminal users typically have dark default fg.
    4. **`preserve_side_content_bg` is a general-purpose flag but only used
    by the theme picker** — if other popups start using side content with
    intentional backgrounds they'll need to opt in explicitly. Not a real
    risk today, just a note for future callers.
  • Fix @mention token parsing in chat composer (#12643)
    Fixes #12175
    
    If a user types an npm package name with multiple `@` symbols like `npx
    -y @foo/bar@latest`, the TUI currently treats this as though it's
    attempting to invoke the file picker.
    
    ### What changed
    
    - **Generalized `@` token parsing**
    - `current_prefixed_token(...)` now treats `@` as a token start **only
    at a whitespace boundary** (or start-of-line).
    - If the cursor is on a nested `@` inside an existing
    whitespace-delimited token (for example `@scope/pkg@latest`), it keeps
    the surrounding token active instead of starting a new token at the
    second `@`.
    - It also avoids misclassifying mid-word usages like `foo@bar` as an `@`
    file token.
    
    - **Enter behavior with file popup**
    - If the file-search popup is open but has **no selected match**,
    pressing `Enter` now closes the popup and falls through to normal submit
    behavior.
    - This prevents pasted strings containing `@...` from blocking
    submission just because file-search was active with no actionable
    selection.
    
    ### Testing
    I manually built and tested the scenarios involved with the bug report
    and related use of `@` mentions to verify no regressions
  • feat: run zsh fork shell tool via shell-escalation (#12649)
    ## Why
    
    This PR switches the `shell_command` zsh-fork path over to
    `codex-shell-escalation` so the new shell tool can use the shared
    exec-wrapper/escalation protocol instead of the `zsh_exec_bridge`
    implementation that was introduced in
    https://github.com/openai/codex/pull/12052. `zsh_exec_bridge` relied on
    UNIX domain sockets, which is not as tamper-proof as the FD-based
    approach in `codex-shell-escalation`.
    
    ## What Changed
    
    - Added a Unix zsh-fork runtime adapter in `core`
    (`core/src/tools/runtimes/shell/unix_escalation.rs`) that:
    - runs zsh-fork commands through
    `codex_shell_escalation::run_escalate_server`
      - bridges exec-policy / approval decisions into `ShellActionProvider`
    - executes escalated commands via a `ShellCommandExecutor` that calls
    `process_exec_tool_call`
    - Updated `ShellRuntime` / `ShellCommandHandler` / tool spec wiring to
    select a `shell_command` backend (`classic` vs `zsh-fork`) while leaving
    the generic `shell` tool path unchanged.
    - Removed the `zsh_exec_bridge`-based session service and deleted
    `core/src/zsh_exec_bridge/mod.rs`.
    - Moved exec-wrapper entrypoint dispatch to `arg0` by handling the
    `codex-execve-wrapper` arg0 alias there, and removed the old
    `codex_core::maybe_run_zsh_exec_wrapper_mode()` hooks from `cli` and
    `app-server` mains.
    - Added the needed `codex-shell-escalation` dependencies for `core` and
    `arg0`.
    
    ## Tests
    
    - `cargo test -p codex-core
    shell_zsh_fork_prefers_shell_command_over_unified_exec`
    - `cargo test -p codex-app-server turn_start_shell_zsh_fork --
    --nocapture`
    - verifies zsh-fork command execution and approval flows through the new
    backend
    - includes subcommand approve/decline coverage using the shared zsh
    DotSlash fixture in `app-server/tests/suite/zsh`
    - To test manually, I added the following to `~/.codex/config.toml`:
    
    ```toml
    zsh_path = "/Users/mbolin/code/codex3/codex-rs/app-server/tests/suite/zsh"
    
    [features]
    shell_zsh_fork = true
    ```
    
    Then I ran `just c` to run the dev build of Codex with these changes and
    sent it the message:
    
    ```
    run `echo $0`
    ```
    
    And it replied with:
    
    ```
      echo $0 printed:
    
      /Users/mbolin/code/codex3/codex-rs/app-server/tests/suite/zsh
    
      In this tool context, $0 reflects the script path used to invoke the shell, not just zsh.
    ```
    
    so the tool appears to be wired up correctly.
    
    ## Notes
    
    - The zsh subcommand-decline integration test now uses `rm` under a
    `WorkspaceWrite` sandbox. The previous `/usr/bin/true` scenario is
    auto-allowed by the new `shell-escalation` policy path, which no longer
    produces subcommand approval prompts.
  • ctrl-L (clears terminal but does not start a new chat) (#12628)
    # ctrl-L
    
    - Clears your terminal window
    - Does not start a new chat
  • feat(core) Introduce Feature::RequestPermissions (#11871)
    ## Summary
    Introduces the initial implementation of Feature::RequestPermissions.
    RequestPermissions allows the model to request that a command be run
    inside the sandbox, with additional permissions, like writing to a
    specific folder. Eventually this will include other rules as well, and
    the ability to persist these permissions, but this PR is already quite
    large - let's get the core flow working and go from there!
    
    <img width="1279" height="541" alt="Screenshot 2026-02-15 at 2 26 22 PM"
    src="https://github.com/user-attachments/assets/0ee3ec0f-02ec-4509-91a2-809ac80be368"
    />
    
    ## Testing
    - [x] Added tests
    - [x] Tested locally
    - [x] Feature
  • fix: replay after /agent (#12663)
    Filter the events after a`/agent` replay to prevent replaying decision
    events
  • chore: rm hardcoded PRESETS list (#12650)
    rm `PRESETS` list harcoded in `model_presets` as we now have bundled
    `models.json` with equivalent info.
    
    update logic to rely on bundled models instead, update tests.
  • Add skill approval event/response (#12633)
    Set the stage for skill-level permission approval in addition to
    command-level.
    
    Behind a feature flag.
  • feat(core): persist network approvals in execpolicy (#12357)
    ## Summary
    Persist network approval allow/deny decisions as `network_rule(...)`
    entries in execpolicy (not proxy config)
    
    It adds `network_rule` parsing + append support in `codex-execpolicy`,
    including `decision="prompt"` (parse-only; not compiled into proxy
    allow/deny lists)
    - compile execpolicy network rules into proxy allow/deny lists and
    update the live proxy state on approval
    - preserve requirements execpolicy `network_rule(...)` entries when
    merging with file-based execpolicy
    - reject broad wildcard hosts (for example `*`) for persisted
    `network_rule(...)`
  • voice transcription (#3381)
    Adds voice transcription on press-and-hold of spacebar.
    
    
    https://github.com/user-attachments/assets/85039314-26f3-46d1-a83b-8c4a4a1ecc21
    
    ---------
    
    Co-authored-by: Codex <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
    Co-authored-by: David Zbarsky <zbarsky@openai.com>
  • fix: show command running in background terminal in details under status indicator (#12549)
    #### What
    Display in-progress background terminal command in `status.details`
    (right under header) rather than inline, as it gets cut off currently.
    
    ###### Before
    <img width="993" height="395" alt="image"
    src="https://github.com/user-attachments/assets/6792b666-8184-40f7-bf29-409bb06c21d5"
    />
    
    ###### After
    <img width="469" height="137" alt="image"
    src="https://github.com/user-attachments/assets/4d6a2481-bd19-4333-8c1a-92f521b09b3d"
    />
    
    #### Tests
    Added/updated tests
  • fix(tui): queue steer Enter while final answer is still streaming to prevent dead state (#12569)
    ## Summary
    This fixes a TUI race (https://github.com/openai/codex/issues/11008)
    where pressing Enter with Steer enabled while the assistant is still
    streaming the final answer could put Codex into a non-recoverable
    “running” state (no further prompts handled until exiting and resuming).
    
    ## Root Cause
    In steer mode, `InputResult::Submitted` could submit immediately even
    while a final-answer stream was active. That immediate submission races
    with turn completion and can strand turn state.
    
    ## Fix
    When handling `InputResult::Submitted`, we now queue instead of
    immediate-submit if a final-answer stream is active
    (`stream_controller.is_some()`).
    
    This keeps behavior deterministic:
    - Prompt is preserved in the queue.
    - `on_task_complete()` drains queued input through
    `maybe_send_next_queued_input()`.
    - Follow-up prompts continue in FIFO order after completion.
    
    ## Why this resolves the “dead mode”
    The problematic timing window is now converted into queueing, so prompts
    entered during final streaming are not lost and are processed after the
    current output ends. The model continues handling prompts normally
    without requiring `/quit` + `resume`.
    
    ## Tests
    Added regression coverage in `tui/src/chatwidget/tests.rs`:
    
    - `steer_enter_queues_while_final_answer_stream_is_active`
    - `steer_enter_during_final_stream_preserves_follow_up_prompts_in_order`
    
    Both fail on old behavior and pass with this fix.
  • fix(tui): recover on owned wrap mapping mismatch (#12609)
    ## Summary
    
    - Replace the `panic!` in `map_owned_wrapped_line_to_range` with a
    recoverable flow that skips synthetic leading characters, logs a warning
    on mid-line mismatch, and returns the mapped prefix range instead of
    crashing
    - Fixes a crash when `textwrap` produces owned lines with synthetic
    indent prefixes (e.g. non-space indents via
    `initial_indent`/`subsequent_indent`)
    
    ## Test plan
    
    - [x] Added unit test for direct mismatch recovery
    (`map_owned_wrapped_line_to_range_recovers_on_non_prefix_mismatch`)
    - [x] Added end-to-end `wrap_ranges` test with non-space indents that
    forces owned wrapped lines and validates full source reconstruction
    - [x] Verify no regressions in existing `wrapping.rs` tests (`cargo test
    -p codex-tui`)
  • fix: add ellipsis for truncated status indicator (#12540)
    #### What
    
    - Add ellipsis truncation of the status indicator, similar to equivalent
    truncation done in the footer.
    - Extract truncation helpers into separate file
    
    
    
    https://github.com/user-attachments/assets/a2d5f22f-8adc-456e-8059-97359194c25c
    
    
    #### Tests
    Updated relevant snapshot tests
  • tweaked /clear to support clear + new chat, also fix minor bug for macos terminal (#12520)
    # /clear feature! 
    
    Use /clear to start a new chat with Codex on a clean terminal!
  • remove feature flag collaboration modes (#12028)
    All code should go in the direction that steer is enabled
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Add C# syntax option to highlight selections (#12511)
    Summary
    - map csharp/c-sharp aliases to the existing C# syntax in the highlight
    matcher
    - ensure the extension list and tests include .cs and the new aliases so
    coverage stays accurate
    
    Testing
    
    <img width="543" height="266" alt="image"
    src="https://github.com/user-attachments/assets/e6c8a42f-649c-4c30-b574-421b4287534c"
    />
  • Sort themes case-insensitively in picker (#12509)
    ## Summary
    - order bundled and custom themes together by name while keeping entries
    stable across platforms
    - update the theme fixture names and tests to assert case-insensitive
    ordering
  • feat(tui): support Alt-d delete-forward-word (#12455)
    Alt-d should delete the next word. It didn’t. Now it does. Added a small
    test so it stays that way.
    
    Details:
    File updated:
    [codex-rs/tui/src/bottom_pane/textarea.rs](./codex-rs/tui/src/bottom_pane/textarea.rs)
    Test added: delete_forward_word_alt_d — verifies Alt-d deletes the next
    word and keeps the cursor position correct.
    
    Solves  Issue #12453
  • Handle orphan exec ends without clobbering active exploring cell (#12313)
    Summary
    - distinguish exec end handling targets (active tracking, active orphan
    history, new cell) so unified exec responses don’t clobber unrelated
    exploring cells
    - ensure orphan ends flush existing exploring history when complete,
    insert standalone history entries, and keep active cells correct
    - add regression tests plus a snapshot covering the new behavior and
    expose the ExecCell completion result for verification
    
    Fix for https://github.com/openai/codex/issues/12278
    
    ---------
    
    Co-authored-by: Josh McKinney <joshka@openai.com>
  • feat(tui) /clear (#12444)
    # /clear feature! 
    
    /clear will clear your terminal while preserving the context/state of
    the thread.
  • feat(tui): syntax highlighting via syntect with theme picker (#11447)
    ## Summary
    
    Adds syntax highlighting to the TUI for fenced code blocks in markdown
    responses and file diffs, plus a `/theme` command with live preview and
    persistent theme selection. Uses syntect (~250 grammars, 32 bundled
    themes, ~1 MB binary cost) — the same engine behind `bat`, `delta`, and
    `xi-editor`. Includes guardrails for large inputs, graceful fallback to
    plain text, and SSH-aware clipboard integration for the `/copy` command.
    
    <img width="1554" height="1014" alt="image"
    src="https://github.com/user-attachments/assets/38737a79-8717-4715-b857-94cf1ba59b85"
    />
    
    <img width="2354" height="1374" alt="image"
    src="https://github.com/user-attachments/assets/25d30a00-c487-4af8-9cb6-63b0695a4be7"
    />
    
    ## Problem
    
    Code blocks in the TUI (markdown responses and file diffs) render
    without syntax highlighting, making it hard to scan code at a glance.
    Users also have no way to pick a color theme that matches their terminal
    aesthetic.
    
    ## Mental model
    
    The highlighting system has three layers:
    
    1. **Syntax engine** (`render::highlight`) -- a thin wrapper around
    syntect + two-face. It owns a process-global `SyntaxSet` (~250 grammars)
    and a `RwLock<Theme>` that can be swapped at runtime. All public entry
    points accept `(code, lang)` and return ratatui `Span`/`Line` vectors or
    `None` when the language is unrecognized or the input exceeds safety
    guardrails.
    
    2. **Rendering consumers** -- `markdown_render` feeds fenced code blocks
    through the engine; `diff_render` highlights Add/Delete content as a
    whole file and Update hunks per-hunk (preserving parser state across
    hunk lines). Both callers fall back to plain unstyled text when the
    engine returns `None`.
    
    3. **Theme lifecycle** -- at startup the config's `tui.theme` is
    resolved to a syntect `Theme` via `set_theme_override`. At runtime the
    `/theme` picker calls `set_syntax_theme` to swap themes live; on cancel
    it restores the snapshot taken at open. On confirm it persists `[tui]
    theme = "..."` to config.toml.
    
    ## Non-goals
    
    - Inline diff highlighting (word-level change detection within a line).
    - Semantic / LSP-backed highlighting.
    - Theme authoring tooling; users supply standard `.tmTheme` files.
    
    ## Tradeoffs
    
    | Decision | Upside | Downside |
    | ------------------------------------------------ |
    ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    |
    -----------------------------------------------------------------------------------------------------------------------
    |
    | syntect over tree-sitter / arborium | ~1 MB binary increase for ~250
    grammars + 32 themes; battle-tested crate powering widely-used tools
    (`bat`, `delta`, `xi-editor`). tree-sitter would add ~12 MB for 20-30
    languages or ~35 MB for full coverage. | Regex-based; less structurally
    accurate than tree-sitter for some languages (e.g. language injections
    like JS-in-HTML). |
    | Global `RwLock<Theme>` | Enables live `/theme` preview without
    threading Theme through every call site | Lock contention risk
    (mitigated: reads vastly outnumber writes, single UI thread) |
    | Skip background / italic / underline from themes | Terminal BG
    preserved, avoids ugly rendering on some themes | Themes that rely on
    these properties lose fidelity |
    | Guardrails: 512 KB / 10k lines | Prevents pathological stalls on huge
    diffs or pastes | Very large files render without color |
    
    ## Architecture
    
    ```
    config.toml  ─[tui.theme]─>  set_theme_override()  ─>  THEME (RwLock)
                                                                  │
                      ┌───────────────────────────────────────────┘
                      │
      markdown_render ─── highlight_code_to_lines(code, lang) ─> Vec<Line>
      diff_render     ─── highlight_code_to_styled_spans(code, lang) ─> Option<Vec<Vec<Span>>>
                      │
                      │   (None ⇒ plain text fallback)
                      │
      /theme picker   ─── set_syntax_theme(theme)    // live preview swap
                      ─── current_syntax_theme()      // snapshot for cancel
                      ─── resolve_theme_by_name(name) // lookup by kebab-case
    ```
    
    Key files:
    
    - `tui/src/render/highlight.rs` -- engine, theme management, guardrails
    - `tui/src/diff_render.rs` -- syntax-aware diff line wrapping
    - `tui/src/theme_picker.rs` -- `/theme` command builder
    - `tui/src/bottom_pane/list_selection_view.rs` -- side content panel,
    callbacks
    - `core/src/config/types.rs` -- `Tui::theme` field
    - `core/src/config/edit.rs` -- `syntax_theme_edit()` helper
    
    ## Observability
    
    - `tracing::warn` when a configured theme name cannot be resolved.
    - `Config::startup_warnings` surfaces the same message as a TUI banner.
    - `tracing::error` when persisting theme selection fails.
    
    ## Tests
    
    - Unit tests in `highlight.rs`: language coverage, fallback behavior,
    CRLF stripping, style conversion, guardrail enforcement, theme name
    mapping exhaustiveness.
    - Unit tests in `diff_render.rs`: snapshot gallery at multiple terminal
    sizes (80x24, 94x35, 120x40), syntax-highlighted wrapping, large-diff
    guardrail, rename-to-different-extension highlighting, parser state
    preservation across hunk lines.
    - Unit tests in `theme_picker.rs`: preview rendering (wide + narrow),
    dim overlay on deletions, subtitle truncation, cancel-restore, fallback
    for unavailable configured theme.
    - Unit tests in `list_selection_view.rs`: side layout geometry, stacked
    fallback, buffer clearing, cancel/selection-changed callbacks.
    - Integration test in `lib.rs`: theme warning uses the final
    (post-resume) config.
    
    ## Cargo Deny: Unmaintained Dependency Exceptions
    
    This PR adds two `cargo deny` advisory exceptions for transitive
    dependencies pulled in by `syntect v5.3.0`:
    
    | Advisory | Crate | Status |
    |----------|-------|--------|
    | RUSTSEC-2024-0320 | `yaml-rust` | Unmaintained (maintainer
    unreachable) |
    | RUSTSEC-2025-0141 | `bincode` | Unmaintained (development ceased;
    v1.3.3 considered complete) |
    
    **Why this is safe in our usage:**
    
    - Neither advisory describes a known security vulnerability. Both are
    "unmaintained" notices only.
    - `bincode` is used by syntect to deserialize pre-compiled syntax sets.
    Again, these are **static vendored artifacts** baked into the binary at
    build time. No user-supplied bincode data is ever deserialized. - Attack
    surface is zero for both crates; exploitation would require a
    supply-chain compromise of our own build artifacts.
    - These exceptions can be removed when syntect migrates to `yaml-rust2`
    and drops `bincode`, or when alternative crates are available upstream.
  • fix(tui): preserve URL clickability across all TUI views (#12067)
    ## Problem
    
    Long URLs containing `/` and `-` characters are split across multiple
    terminal lines by `textwrap`'s default hyphenation rules. This breaks
    terminal link detection: emulators can no longer identify the URL as
    clickable, and copy-paste yields a truncated fragment. The issue affects
    every view that renders user or agent text — exec output, history cells,
    markdown, the app-link setup screen, and the VT100 scrollback path.
    
    A secondary bug compounds the first: `desired_height()` calculations
    count logical lines rather than viewport rows. When a URL overflows its
    line and wraps visually, the height budget is too small, causing content
    to clip or leave gaps.
    
    Here is how the complete URL is interpreted by the terminal before
    (first line only) and after (complete URL):
    
    | Before | After |
    |---|---|
    | <img width="777" height="1002" alt="Screenshot 2026-02-17 at 7 59 11
    PM"
    src="https://github.com/user-attachments/assets/193a89a0-7e56-49c5-8b76-53499a76e7e3"
    /> | <img width="777" height="1002" alt="Screenshot 2026-02-17 at 7 58
    40 PM"
    src="https://github.com/user-attachments/assets/0b9b4c14-aafb-439f-9ffe-f6bba556f95e"
    /> |
    
    ## Mental model
    
    The TUI now treats URL-like tokens as atomic units that must never be
    split by the wrapping engine. Every call site that previously used
    `word_wrap_*` has been migrated to `adaptive_wrap_*`, which inspects
    each line for URL-like tokens and switches wrapping strategy
    accordingly:
    
    - **Non-URL lines** follow the existing `textwrap` path unchanged (word
    boundaries, optional indentation, hyphenation).
    - **URL-only lines** (with at most decorative markers like `│`, `-`,
    `1.`) are emitted unwrapped so terminal link detection works; ratatui's
    `Wrap { trim: false }` handles the final character wrap at render time.
    - **Mixed lines** (URL + substantive non-URL prose) flow through
    `adaptive_wrap_line` so prose wraps naturally at word boundaries while
    URL tokens remain unsplit.
    
    Height measurement everywhere now delegates to
    `Paragraph::line_count(width)`, which accounts for the visual row cost
    of overflowed lines. This single source of truth replaces ad-hoc line
    counting in individual cells.
    
    For terminal scrollback (the VT100 path that prints history when the TUI
    exits), URL-only lines are emitted unwrapped so the terminal's own link
    detector can find them. Mixed URL+prose lines use adaptive wrapping so
    surrounding text wraps naturally. Continuation rows are pre-cleared to
    avoid stale content artifacts.
    
    ## Non-goals
    
    - Full RFC 3986 URL parsing. The detector is a conservative heuristic
    that covers `scheme://host`, bare domains (`example.com/path`),
    `localhost:port`, and IPv4 hosts. IPv6 (`[::1]:8080`) and exotic schemes
    are intentionally excluded from v1.
    - Changing wrapping behavior for non-URL content.
    - Reflowing or reformatting existing terminal scrollback on resize.
    
    ## Tradeoffs
    
    | Decision | Upside | Downside |
    |----------|--------|----------|
    | Heuristic URL detection vs. full parser | Fast, zero-alloc on the hot
    path; conservative enough to reject file paths like `src/main.rs` |
    False negatives on obscure URL formats (they get split as before) |
    | Adaptive (three-path) wrapping | Non-URL lines are untouched — no
    behavior change, no perf cost; mixed lines wrap prose naturally while
    preserving URLs | Three wrapping strategies to reason about when
    debugging layout |
    | Row-based truncation with line-unit ellipsis | Accurate viewport
    budget; stable "N lines omitted" count across terminal widths |
    `truncate_lines_middle` is more complex (must compute per-line row cost)
    |
    | Unwrapped URL-only lines in scrollback | Terminal emulators detect
    clickable links; copy-paste gets the full URL | TUI and scrollback
    formatting diverge for URL-only lines |
    | Default `desired_height` via `Paragraph::line_count` | DRY — most
    cells inherit correct measurement | Cells with custom layout must
    remember to override |
    
    ## Architecture
    
    ```mermaid
    flowchart TD
        A["adaptive_wrap_*()"] --> B{"line_contains_url_like?"}
        B -- No URL tokens --> C["word_wrap_line<br/>(textwrap default)"]
        B -- Has URL tokens --> D{"mixed URL + prose?"}
        D -- "URL-only<br/>(+ decorative markers)" --> E["emit unwrapped<br/>(terminal char-wraps)"]
        D -- "Mixed<br/>(URL + substantive text)" --> F["adaptive_wrap_line<br/>(AsciiSpace + custom WordSplitter)"]
        C --> G["Paragraph::line_count(w)<br/>(single height truth)"]
        E --> G
        F --> G
    ```
    
    **Changed files:**
    
    | File | Role |
    |------|------|
    | `wrapping.rs` | URL detection heuristics, mixed-line detection,
    `adaptive_wrap_*` functions, custom `WordSplitter` |
    | `exec_cell/render.rs` | Row-aware `truncate_lines_middle`, adaptive
    wrapping for command/output display |
    | `history_cell.rs` | Migrate all cell types to `adaptive_wrap_*`;
    default `desired_height` via `Paragraph::line_count` |
    | `insert_history.rs` | Three-path scrollback wrapping (unwrapped
    URL-only, adaptive mixed, word-wrapped text); continuation row clearing
    |
    | `app_link_view.rs` | Adaptive wrapping for setup URL; `desired_height`
    via `Paragraph::line_count` |
    | `markdown_render.rs` | Adaptive wrapping in `finish_paragraph` |
    | `model_migration.rs` | Viewport-aware wrapping for narrow-pane
    markdown |
    | `pager_overlay.rs` | `Wrap { trim: false }` for transcript and
    streaming chunks |
    | `queued_user_messages.rs` | Migrate to `adaptive_wrap_lines` |
    | `status/card.rs` | Migrate to `adaptive_wrap_lines` |
    
    ## Observability
    
    - **Ellipsis message** in truncated exec output reports omitted count in
    logical lines (stable across resize) rather than viewport rows
    (fluctuates).
    - URL detection is deterministic and stateless — no hidden caching or
    memoization to go stale.
    - Height mismatch bugs surface immediately as visual clipping or gaps;
    the `Paragraph::line_count` path is the same code ratatui uses at render
    time, so measurement and rendering cannot diverge.
    
    ## Tests
    
    26 new unit tests across 7 files, covering:
    
    - **URL integrity**: assert a URL-like token appears on exactly one
    rendered line (not split across two).
    - **Height accuracy**: compare `desired_height()` against
    `Paragraph::line_count()` for URL-containing content.
    - **Row-aware truncation**: verify ellipsis counts logical lines and
    output fits within the row budget.
    - **Scrollback rendering**: VT100 backend tests confirm prefix and URL
    land on the same row; continuation rows are cleared; mixed URL+prose
    lines wrap prose while preserving URL tokens.
    - **Mixed URL+prose detection**: `line_has_mixed_url_and_non_url_tokens`
    correctly distinguishes lines with substantive non-URL text from lines
    with only decorative markers alongside a URL.
    - **Heuristic correctness**: positive matches (`https://...`,
    `example.com/path`, `localhost:3000/api`, `192.168.1.1:8080/health`) and
    negative matches (`src/main.rs`, `foo/bar`, `hello-world`).
    
    ## Risks and open items
    
    1. **URL-like tokens in code output** (e.g. `example.com/api` inside a
    JSON blob) will trigger URL-preserving wrap on that line. This is
    acceptable — the worst case is a slightly wider line, not broken output.
    2. **Very long non-URL tokens on a URL line** can only break at
    character boundaries (the custom splitter emits all char indices for
    non-URL words). On extremely narrow terminals this could overflow, but
    narrow terminals already degrade gracefully.
    3. **No IPv6 support** — `[::1]:8080/path` will be treated as a non-URL
    and may get split. Can be added later without API changes.
    
    Fixes #5457
  • Prevent replayed runtime events from forcing active status (#12420)
    Fixes #11852
    
    Resume replay was applying transient runtime events (`TurnStarted`,
    `StreamError`) as if they were live, which could leave the TUI stuck in
    a stale `Working` / `Reconnecting...` state after resuming an
    interrupted reconnect.
    
    This change makes replay transcript-oriented for these events by:
    - skipping retry-status restoration for replayed non-stream events
    - ignoring replayed `TurnStarted` for task-running state
    - ignoring replayed `StreamError` for reconnect/status UI
    
    Also adds TUI regression tests and snapshot coverage for the interrupted
    reconnect replay case.
  • chore: remove codex-core public protocol/shell re-exports (#12432)
    ## Why
    
    `codex-rs/core/src/lib.rs` re-exported a broad set of types and modules
    from `codex-protocol` and `codex-shell-command`. That made it easy for
    workspace crates to import those APIs through `codex-core`, which in
    turn hides dependency edges and makes it harder to reduce compile-time
    coupling over time.
    
    This change removes those public re-exports so call sites must import
    from the source crates directly. Even when a crate still depends on
    `codex-core` today, this makes dependency boundaries explicit and
    unblocks future work to drop `codex-core` dependencies where possible.
    
    ## What Changed
    
    - Removed public re-exports from `codex-rs/core/src/lib.rs` for:
    - `codex_protocol::protocol` and related protocol/model types (including
    `InitialHistory`)
      - `codex_protocol::config_types` (`protocol_config_types`)
    - `codex_shell_command::{bash, is_dangerous_command, is_safe_command,
    parse_command, powershell}`
    - Migrated workspace Rust call sites to import directly from:
      - `codex_protocol::protocol`
      - `codex_protocol::config_types`
      - `codex_protocol::models`
      - `codex_shell_command`
    - Added explicit `Cargo.toml` dependencies (`codex-protocol` /
    `codex-shell-command`) in crates that now import those crates directly.
    - Kept `codex-core` internal modules compiling by using `pub(crate)`
    aliases in `core/src/lib.rs` (internal-only, not part of the public
    API).
    - Updated the two utility crates that can already drop a `codex-core`
    dependency edge entirely:
      - `codex-utils-approval-presets`
      - `codex-utils-cli`
    
    ## Verification
    
    - `cargo test -p codex-utils-approval-presets`
    - `cargo test -p codex-utils-cli`
    - `cargo check --workspace --all-targets`
    - `just clippy`
  • Improve Plan mode reasoning selection flow (#12303)
    Addresses https://github.com/openai/codex/issues/11013
    
    ## Summary
    - add a Plan implementation path in the TUI that lets users choose
    reasoning before switching to Default mode and implementing
    - add Plan-mode reasoning scope handling (Plan-only override vs
    all-modes default), including config/schema/docs plumbing for
    `plan_mode_reasoning_effort`
    - remove the hardcoded Plan preset medium default and make the reasoning
    popup reflect the active Plan override as `(current)`
    - split the collaboration-mode switch notification UI hint into #12307
    to keep this diff focused
    
    If I have `plan_mode_reasoning_effort = "medium"` set in my
    `config.toml`:
    <img width="699" height="127" alt="Screenshot 2026-02-20 at 6 59 37 PM"
    src="https://github.com/user-attachments/assets/b33abf04-6b7a-49ed-b2e9-d24b99795369"
    />
    
    If I don't have `plan_mode_reasoning_effort` set in my `config.toml`:
    <img width="704" height="129" alt="Screenshot 2026-02-20 at 7 01 51 PM"
    src="https://github.com/user-attachments/assets/88a086d4-d2f1-49c7-8be4-f6f0c0fa1b8d"
    />
    
    ## Codex author
    `codex resume 019c78a2-726b-7fe3-adac-3fa4523dcc2a`
  • Wire realtime api to core (#12268)
    - Introduce `RealtimeConversationManager` for realtime API management 
    - Add `op::conversation` to start conversation, insert audio, insert
    text, and close conversation.
    - emit conversation lifecycle and realtime events.
    - Move shared realtime payload types into codex-protocol and add core
    e2e websocket tests for start/replace/transport-close paths.
    
    Things to consider:
    - Should we use the same `op::` and `Events` channel to carry audio? I
    think we should try this simple approach and later we can create
    separate one if the channels got congested.
    - Sending text updates to the client: we can start simple and later
    restrict that.
    - Provider auth isn't wired for now intentionally
  • fix(tui): queued-message edit shortcut unreachable in some terminals (#12240)
    ## Problem
    
    The TUI's "edit queued message" shortcut (Alt+Up) is either silently
    swallowed or recognized as another key combination by Apple Terminal,
    Warp, and VSCode's integrated terminal on macOS. Users in those
    environments see the hint but pressing the keys does nothing.
    
    ## Mental model
    
    When a model turn is in progress the user can still type follow-up
    messages. These are queued and displayed below the composer with a hint
    line showing how to pop the most recent one back into the editor. The
    hint text and the actual key handler must agree on which shortcut is
    used, and that shortcut must actually reach the TUI—i.e. it must not be
    intercepted by the host terminal.
    
    Three terminals are known to intercept Alt+Up: Apple Terminal (remaps it
    to cursor movement), Warp (consumes it for its own command palette), and
    VSCode (maps it to "move line up"). For these we use Shift+Left instead.
    
    <p align="center">
    <img width="283" height="182" alt="image"
    src="https://github.com/user-attachments/assets/4a9c5d13-6e47-4157-bb41-28b4ce96a914"
    />
    </p>
    
    | macOS Native Terminal | Warp | VSCode Terminal |
    |---|---|---|
    | <img width="1557" height="1010" alt="SCR-20260219-kigi"
    src="https://github.com/user-attachments/assets/f4ff52f8-119e-407b-a3f3-52f564c36d70"
    /> | <img width="1479" height="1261" alt="SCR-20260219-krrf"
    src="https://github.com/user-attachments/assets/5807d7c4-17ae-4a2b-aa27-238fd49d90fd"
    /> | <img width="1612" height="1312" alt="SCR-20260219-ksbz"
    src="https://github.com/user-attachments/assets/1cedb895-6966-4d63-ac5f-0eea0f7057e8"
    /> |
    
    ## Non-goals
    
    - Making the binding user-configurable at runtime (deferred to a broader
    keybinding-config effort).
    - Remapping any other shortcuts that might be terminal-specific.
    
    ## Tradeoffs
    
    - **Exhaustive match instead of a wildcard default.** The
    `queued_message_edit_binding_for_terminal` function explicitly lists
    every `TerminalName` variant. This is intentional: adding a new terminal
    to the enum will produce a compile error, forcing the author to decide
    which binding that terminal should use.
    
    - **Binding lives on `ChatWidget`, hint lives on `QueuedUserMessages`.**
    The key event handler that actually acts on the press is in
    `ChatWidget`, but the rendered hint text is inside `QueuedUserMessages`.
    These are kept in sync by `ChatWidget` calling
    `bottom_pane.set_queued_message_edit_binding(self.queued_message_edit_binding)`
    during construction. A mismatch would show the wrong hint but would not
    lose data.
    
    ## Architecture
    
    ```mermaid
      graph TD
          TI["terminal_info().name"] --> FN["queued_message_edit_binding_for_terminal(name)"]
          FN --> KB["KeyBinding"]
          KB --> CW["ChatWidget.queued_message_edit_binding<br/><i>key event matching</i>"]
          KB --> BP["BottomPane.set_queued_message_edit_binding()"]
          BP --> QUM["QueuedUserMessages.edit_binding<br/><i>rendered in hint line</i>"]
    
          subgraph "Special terminals (Shift+Left)"
              AT["Apple Terminal"]
              WT["Warp"]
              VS["VSCode"]
          end
    
          subgraph "Default (Alt+Up)"
              GH["Ghostty"]
              IT["iTerm2"]
              OT["Others…"]
          end
    
          AT --> FN
          WT --> FN
          VS --> FN
          GH --> FN
          IT --> FN
          OT --> FN
    ```
    
    No new crates or public API surface. The only cross-crate dependency
    added is `codex_core::terminal::{TerminalName, terminal_info}`, which
    already existed for telemetry.
    
    ## Observability
    
    No new logging. Terminal detection already emits a `tracing::debug!` log
    line at startup with the detected terminal name, which is sufficient to
    diagnose binding mismatches.
    
    ## Tests
    
    - Existing `alt_up_edits_most_recent_queued_message` test is preserved
    and explicitly sets the Alt+Up binding to isolate from the host
    terminal.
    - New parameterized async tests verify Shift+Left works for Apple
    Terminal, Warp, and VSCode.
    - A sync unit test asserts the mapping table covers the three special
    terminals (Shift+Left) and that iTerm2 still gets Alt+Up.
      
    Fixes #4490
  • Show model/reasoning hint when switching modes (#12307)
    ## Summary
    - show an info message when switching collaboration modes changes the
    effective model or reasoning
    - include the target mode in the message (for example `... for Plan
    mode.`)
    - add TUI tests for model-change and reasoning-only change notifications
    on mode switch
    
    <img width="715" height="184" alt="Screenshot 2026-02-20 at 2 01 40 PM"
    src="https://github.com/user-attachments/assets/18d1beb3-ab87-4e1c-9ada-a10218520420"
    />