Commit Graph

1757 Commits

  • feat(tui): render responsive Markdown tables in TUI (#22052)
    ## Why
    
    The TUI currently treats Markdown tables as ordinary wrapped text, which
    makes table-heavy responses hard to read and brittle across narrow panes
    and terminal resizes.
    
    This change teaches the TUI to render Markdown tables responsively while
    preserving the raw Markdown source needed to re-render streamed and
    finalized transcript content after width changes. The goal is to keep
    tables legible during streaming, after resize, and once a turn has
    finished, without corrupting scrollback ordering.
    
    ## What Changed
    
    - add table detection and responsive table rendering in the Markdown
    renderer
    - render standard tables with Unicode box-drawing borders when the pane
    is wide enough
    - add a vertical readability fallback for constrained or dense tables so
    narrow panes still show each row clearly
    - keep links and `<br>` content inside table cells instead of leaking
    text outside the table
    - avoid table normalization inside fenced or indented code blocks
    - preserve raw streamed Markdown source and keep the active table as a
    mutable tail until finalization
    - consolidate finalized streamed content into source-backed transcript
    cells so post-resize re-rendering stays correct
    - add snapshot and targeted streaming/resize regression coverage for the
    new table behavior
    
    ## How to Test
    
    1. Start Codex TUI from this branch.
    2. Paste this exact prompt:
    `This is a session to test codex, no need to do any thinking, just end
    different markdown tables, with columns exploring different markdown
    contents, like links, bold italic, code, etc. Make them different sizes,
    some 30+ rows, some not and intertwine them with some paragraphs with
    complex formatting as well.`
    3. Confirm the response includes several Markdown tables mixed with
    richly formatted paragraphs.
    4. Confirm wide-enough tables render with box-drawing borders instead of
    plain wrapped pipe text.
    5. Resize the terminal narrower while the answer is still streaming and
    confirm the in-progress table stays coherent instead of duplicating
    headers or leaving broken scrollback behind.
    6. Resize again after the turn finishes and confirm the finalized
    transcript re-renders cleanly at the new width.
    7. In a narrow pane, verify dense tables fall back to the vertical
    per-row layout instead of producing unreadable wrapped columns.
    8. Also verify pipe-heavy fenced code blocks still render as code, not
    as tables.
    
    Targeted tests:
    - `cargo test -p codex-tui table_readability_fallback --no-fail-fast`
    - `cargo test -p codex-tui markdown_render --no-fail-fast`
    - `cargo test -p codex-tui streaming::controller --no-fail-fast`
    - `cargo test -p codex-tui table_resize_lifecycle --no-fail-fast`
    
    ## Docs
    
    No developer docs update appears necessary.
  • fix(tui): preserve Shift+Enter in tmux csi-u panes (#21943)
    ## Why
    
    Inside tmux, `Shift+Enter` can still reach Codex as a plain `Enter` even
    when tmux has extended keys enabled. In `csi-u` tmux panes, Codex needs
    to request `modifyOtherKeys` mode 2 so tmux moves the pane from `VT10x`
    into extended-key mode and preserves the Shift modifier. Without that
    extra request, composer `Shift+Enter` submits the draft instead of
    inserting a newline.
    
    Fixes #21699.
    
    ## What Changed
    
    - Detect tmux sessions and read the active `extended-keys-format`,
    preferring the pane-local value before falling back to the global
    option.
    - Request `modifyOtherKeys` mode 2 for tmux panes using `csi-u` extended
    keys, and reset it when restoring keyboard reporting.
    - Add unit coverage for tmux detection, the format gate, and the emitted
    `modifyOtherKeys` escape sequence.
    
    ## How to Test
    
    1. In tmux, configure:
       ```tmux
       set-option -g extended-keys on
       set-option -g extended-keys-format csi-u
       ```
    2. Start Codex in a fresh tmux pane from this branch.
    3. From another pane, confirm the Codex pane reports `mode=Ext 2`:
       ```bash
    tmux list-panes -a -F '#{session_name}:#{window_index}.#{pane_index}
    mode=#{pane_key_mode} cmd=#{pane_current_command}'
       ```
    4. Type a draft in the composer and press `Shift+Enter`; confirm it
    inserts a newline instead of submitting.
    5. Also confirm plain `Enter` still submits as before.
    
    Targeted tests:
    - `cargo test -p codex-tui`
    
    ## Notes
    
    - Manual verification used both real `Shift+Enter` in iTerm2/tmux and
    `tmux send-keys ... S-Enter` to confirm the tmux pane changes from
    `VT10x` to `Ext 2` and preserves newline behavior.
    - On this checkout, the broader `codex-tui` test run currently reaches
    unrelated existing failures in `status::tests::*` plus a later stack
    overflow in
    `tests::fork_last_filters_latest_session_by_cwd_unless_show_all`.
  • Split ChatWidget state into focused modules (#21866)
    ## Summary
    
    `ChatWidget` has been carrying several independent domains in one large
    state bag: transcript bookkeeping, turn lifecycle, queued input, status
    surfaces, connectors, review mode, and protocol dispatch. That makes
    otherwise-local changes hard to reason about because unrelated fields
    and side effects live beside each other in `chatwidget.rs`.
    
    This is the first cleanup PR in a larger decomposition effort. It does
    not try to make `chatwidget.rs` small in one sweep; instead, it
    establishes focused state boundaries that later handler, popup,
    rendering, and effect-synchronization extractions can build on.
    
    This PR keeps `ChatWidget` as the composition layer while moving focused
    state into smaller `codex-tui` modules. The widget still owns effects
    that touch the bottom pane, app events, command submission, redraw
    scheduling, and terminal-title updates.
    
    ## Changes
    
    - Add focused state modules under `codex-rs/tui/src/chatwidget/` for
    input queues, turn lifecycle, transcript bookkeeping, status state,
    connectors, review mode, and app-server protocol dispatch.
    - Update `ChatWidget` to hold grouped state structs and route
    input/lifecycle/status operations through those focused helpers.
    - Move app-server notification dispatch into `chatwidget/protocol.rs`
    while leaving feature handlers and side effects on `ChatWidget`.
    - Replace the large manual `ChatWidget` test literal with the normal
    constructor plus narrow test overrides, so future state moves do not
    require every field to be restated in test setup.
    - Update existing tests to access the new grouped state or narrower
    helpers without changing snapshot behavior.
    
    ## Longer-term direction
    
    Follow-up PRs can continue shrinking `chatwidget.rs` by moving behavior,
    not just state, into focused modules:
    
    - Extract input/submission flow, turn/stream handling, and tool-cell
    lifecycles into domain modules that call the new state reducers.
    - Move popup/settings builders and rendering helpers out of the main
    widget file so `ChatWidget` stays focused on composition.
    - Reduce direct `BottomPane` mutation by applying domain-specific sync
    outputs at clearer boundaries.
  • Avoid blocking TUI on agent metadata hydration (#21870)
    ## Why
    
    Fixes #16688.
    
    The TUI currently hydrates collab receiver metadata by awaiting
    `thread/read` before each active-thread notification is rendered. During
    large subagent fan-outs, the embedded app-server can be busy starting
    agents and processing spawn work, so those synchronous metadata reads
    queue behind the fan-out and block the TUI event loop. That makes the UI
    appear frozen even though the underlying agent work can continue.
    
    ## What Changed
    
    - Replaced eager `thread/read` metadata hydration on the active
    notification path with local receiver-thread caching.
    - Kept `ThreadStarted` and picker refreshes as the places that fill in
    agent nickname/role metadata when it is available.
    - Skipped caching receiver threads that are explicitly reported as
    `NotFound`, avoiding live-looking ghost entries for failed stale-agent
    calls.
    - Added TUI tests covering both local receiver caching and `NotFound`
    suppression.
    
    ## Verification
    
    - `cargo test -p codex-tui collab_receiver_notification`
    - `just fix -p codex-tui`
    
    I also ran the full `cargo test -p codex-tui`; the new test passed, but
    the full process later aborted with an unrelated stack overflow in
    `tests::fork_last_filters_latest_session_by_cwd_unless_show_all`.
  • Improve hooks trust flow in TUI (#21755)
    # Why
    Hooks that need trust review were easy to miss, and the existing TUI
    flow made users discover `/hooks` manually before they could decide
    whether to inspect or trust them.
    
    # What
    - add a startup review prompt for new or changed hooks before normal
    composer use
    - add a top-level `t` shortcut in `/hooks` to trust every review-needed
    hook at once
    - make pending-review rows and helper copy use warning styling
    
    ## TUI
    
    ### Startup review interstitial
    
    ```text
    Hooks need review
    2 hooks are new or changed.
    Hooks can run outside the sandbox after you trust them.
    
    › 1. Review hooks
      2. Trust all and continue
      3. Continue without trusting (hooks won't run)
    ```
    
    ### Top-level `/hooks` page when review is needed
    
    ```text
    Hooks
    Lifecycle hooks from config and enabled plugins.
    
    ⚠ 1 hook needs review before it can run.
    
    Event                 Installed   Active   Review   Description
    PreToolUse            1           0        1        Before a tool executes
    ...
    
    Press t to trust all; enter to review hooks; esc to close
    ```
  • fix(tui): improve light-mode selection contrast (#21950)
    ## Why
    
    On light terminal backgrounds, selected rows in several TUI pickers were
    rendered with the same bright cyan accent used on dark themes. Against
    the light menu surface, that made the current selection hard to
    distinguish at a glance.
    
    <table><tr>
    <td>
    <p align="center">Before</p>
    <img width="1109" height="864" alt="SCR-20260509-nmtz"
    src="https://github.com/user-attachments/assets/b31ce0d0-19c2-4bdd-a220-7acc77bd8e8e"
    />
    </td>
    <td>
    <p align="center">After</p>
    <img width="1164" height="844" alt="SCR-20260509-nmox"
    src="https://github.com/user-attachments/assets/7b3fede0-4739-4a9f-a979-cdbb7451841f"
    />
    </td>
    </tr></table>
    
    ## What changed
    
    - Added a shared background-aware accent style for active/selected TUI
    controls.
    - Use a darker cyan-family accent on light backgrounds while preserving
    the existing bright cyan accent on dark or unknown backgrounds.
    - Reused that accent across shared picker rows and the custom
    selection-like surfaces that had drifted separately: picker tabs, hooks
    browsing, external-agent migration choices, and /keymap affordances.
    - Added focused tests for the light/dark accent rule and rendered
    selected-row styling.
    
    ## How to Test
    
    1. Start Codex in a terminal using a light background theme.
    2. Type `/` to open the slash-command picker and move the selection
    through a few rows.
    3. Confirm that the selected row is visibly colored with strong contrast
    instead of blending into the popup surface.
    4. Open `/keymap` and confirm the active tab, selected rows, and picker
    hint accents use the same light-theme accent treatment.
    5. In a dark terminal theme, repeat the slash-picker check and confirm
    the existing bright cyan selection styling is preserved.
    
    Targeted tests:
    - `cargo test -p codex-tui accent_style_uses_`
    - `cargo test -p codex-tui selected_rows_use_the_shared_accent_style`
    - `cargo test -p codex-tui
    selected_event_rows_use_the_shared_accent_style`
    
    Notes:
    - A full `cargo test -p codex-tui` run reached the end of the suite but
    hit an unrelated existing stack overflow in
    `tests::fork_last_filters_latest_session_by_cwd_unless_show_all`.
  • fix(tui): preserve wrapped prose beside URLs (#21760)
    ## Why
    
    Mixed prose lines that contained URLs started taking the URL-preserving
    wrapping path, but that path could split ordinary words mid-token. A
    follow-up issue remained in scrollback insertion: when already-rendered
    indented rows were wrapped again, continuation rows could lose their
    margin and fall back to terminal hard wrapping. Together those bugs made
    normal Markdown output look broken around links, lists, blockquotes, and
    indented content.
    
    Separately, the local argument-comment lint wrappers failed under
    environments that set `PYTHONSAFEPATH=1`, because Python no longer adds
    the script directory to `sys.path` automatically. That prevented the
    lint from reaching Rust callsites at all.
    
    <img width="1778" height="1558" alt="CleanShot 2026-05-09 at 11 51 38"
    src="https://github.com/user-attachments/assets/9274d150-1757-4f1a-89ac-5bdc9997d8cb"
    />
    
    ## What Changed
    
    - Preserve URL tokens without turning every neighboring prose word into
    a character-level split point.
    - Add a mixed URL/prose wrapper that keeps ordinary words whole,
    preserves leading whitespace, and re-splits long non-URL tokens against
    the actual width available on continuation rows.
    - Reuse a rendered history row's leading whitespace as the continuation
    indent when scrollback insertion has to pre-wrap it again.
    - Add regression coverage for markdown wrapping, history-cell rendering,
    scrollback continuation margins, leading-indent width accounting, and
    continuation-row re-splitting.
    - Make both argument-comment lint entrypoints explicitly add their own
    directory to `sys.path`, so sibling imports still work when
    `PYTHONSAFEPATH=1`.
    
    ## How to Test
    
    1. Start Codex and render a long Markdown response that mixes prose with
    inline links, blockquotes, lists, and indented code-like text.
    2. Confirm that ordinary words next to links stay whole instead of
    breaking mid-word.
    3. Resize or replay the transcript and confirm wrapped continuation rows
    keep their expected left margin for blockquotes, lists, and indented
    content.
    4. Run the source argument-comment lint from a shell with
    `PYTHONSAFEPATH=1` and confirm it starts normally instead of failing to
    import `wrapper_common`.
    
    Targeted tests:
    - `cargo test -p codex-tui mixed_line --lib`
    - `cargo test -p codex-tui preserves_prefix_on_wrapped_rows --lib`
    - `cargo test -p codex-tui
    agent_markdown_cell_does_not_split_words_after_inline_markdown --lib`
    - `cargo test -p codex-tui
    mixed_url_markdown_wraps_prose_without_splitting_words_snapshot --lib`
    - `python3 tools/argument-comment-lint/test_wrapper_common.py`
    - `just argument-comment-lint-from-source -p codex-tui -- --lib`
    
    Notes:
    - `cargo test -p codex-tui` currently reaches the new tests
    successfully, then still aborts in the pre-existing
    `tests::fork_last_filters_latest_session_by_cwd_unless_show_all`
    stack-overflow failure.
  • [codex] Lowercase TUI service tier commands (#21906)
    ## Why
    
    Service-tier slash commands are built from model-catalog metadata. If
    the catalog returns a name like `Fast`, the TUI currently exposes
    `/Fast` and exact dispatch expects that casing, which is inconsistent
    with the lowercase command style used elsewhere.
    
    ## What
    
    - Lowercase service-tier command names when converting catalog tiers
    into `ServiceTierCommand` values.
    - Add regression coverage that seeds a catalog tier named `Fast` and
    expects the generated command to be `fast`.
    
    ## Testing
    
    Not run locally per repo instruction; PR CI should run the new
    `service_tier_commands_lowercase_catalog_names` coverage.
  • [codex] request desktop attestation from app (#20619)
    ## Summary
    
    TL;DR: teaches `codex-rs` / app-server to request a desktop-provided
    attestation token and attach it as `x-oai-attestation` on the scoped
    ChatGPT Codex request paths.
    
    ![DeviceCheck attestation
    interface](https://raw.githubusercontent.com/openai/codex/dev/jm/devicecheck-diagram-assets/pr-assets/devicecheck-attestation-interface.png)
    
    ## Details
    
    This PR teaches the Codex app-server runtime how to request and attach
    an attestation token. It does not generate DeviceCheck tokens directly;
    instead, it relies on the connected desktop app to advertise that it can
    generate attestation and then asks that app for a fresh header value
    when needed.
    
    The flow is:
    
    1. The Codex desktop app connects to app-server.
    2. During `initialize`, the app can advertise that it supports
    `requestAttestation`.
    3. Before app-server calls selected ChatGPT Codex endpoints, it sends
    the internal server request `attestation/generate` to the app.
    4. app-server receives a pre-encoded header value back.
    5. app-server forwards that value as `x-oai-attestation` on the scoped
    outbound requests.
    
    The code in this repo is mostly protocol and runtime plumbing: it adds
    the app-server request/response shape, introduces an attestation
    provider in core, wires that provider into Responses / compaction /
    realtime setup paths, and covers the intended scoping with tests. The
    signed macOS DeviceCheck generation remains owned by the desktop app PR.
    
    ## Related PR
    
    - Codex desktop app implementation:
    https://github.com/openai/openai/pull/878649
    
    ## Validation
    
    <details>
    <summary>Tests run</summary>
    
    ```sh
    cargo test -p codex-app-server-protocol
    cargo test -p codex-core attestation --lib
    cargo test -p codex-app-server --lib attestation
    ```
    
    Also ran:
    
    ```sh
    just fix -p codex-core
    just fix -p codex-app-server
    just fix -p codex-app-server-protocol
    just fmt
    just write-app-server-schema
    ```
    
    </details>
    
    <details>
    <summary>E2E DeviceCheck validation</summary>
    
    First validated the signed desktop app boundary directly: launched a
    packaged signed `Codex.app`, sent `attestation/generate`, decoded the
    returned `v1.` attestation header, and validated the extracted
    DeviceCheck token with `personal/jm/verify_devicecheck_token.py` using
    bundle ID `com.openai.codex`. Apple returned `status_code: 200` and
    `is_ok: true`.
    
    Then ran the fuller app + app-server flow. The packaged `Codex.app`
    launched a current-branch app-server via `CODEX_CLI_PATH`, and a local
    MITM proxy intercepted outbound `chatgpt.com` traffic. The app-server
    requested `attestation/generate` from the real Electron app process, and
    the intercepted `/backend-api/codex/responses` traffic included
    `x-oai-attestation` on both routes:
    
    ```text
    GET  /backend-api/codex/responses  Upgrade: websocket  x-oai-attestation: present
    POST /backend-api/codex/responses  Upgrade: none       x-oai-attestation: present
    ```
    
    The captured header decoded to a DeviceCheck token that also validated
    with Apple for `com.openai.codex` (`status_code: 200`, `is_ok: true`,
    team `2DC432GLL2`).
    
    </details>
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Load configured environments from CODEX_HOME (#20667)
    ## Why
    
    The earlier PRs add stdio transport support and the config-backed
    environment provider, but the feature remains inert until normal Codex
    entrypoints construct `EnvironmentManager` with enough context to
    discover `CODEX_HOME/environments.toml`. This final stack PR activates
    the provider while preserving the legacy `CODEX_EXEC_SERVER_URL`
    fallback when no environments file exists.
    
    **Stack position:** this is PR 5 of 5. It is the product wiring PR that
    activates the configured environment provider added in PR 4.
    
    ## What Changed
    
    - Thread `codex_home` into `EnvironmentManagerArgs`.
    - Change `EnvironmentManager::new(...)` to load the provider from
    `CODEX_HOME`.
    - Preserve legacy behavior by falling back to
    `DefaultEnvironmentProvider::from_env()` when `environments.toml` is
    absent.
    - Make `environments.toml`-backed managers start new threads with all
    configured environments, default first, while keeping the legacy env-var
    path single-default.
    - Update the app-server, TUI, exec, MCP server, connector, prompt-debug,
    and thread-manager-sample callsites to pass `codex_home` and handle
    provider-loading errors.
    
    ## Self-Review Notes
    
    - The multi-environment startup path is intentionally tied to the
    `environments.toml` provider. Using `>1` configured environment as the
    only signal would also expand the legacy `CODEX_EXEC_SERVER_URL`
    provider because it keeps `local` addressable alongside `remote`.
    - The startup environment list is still derived inside
    `EnvironmentManager`; the provider only says whether its snapshot should
    start new threads with all configured environments.
    - The thread-manager sample was updated to pass the current
    `ThreadManager::new(...)` installation id argument so the stack compiles
    under Bazel.
    
    ## Stack
    
    - 1. https://github.com/openai/codex/pull/20663 - Add stdio exec-server
    listener
    - 2. https://github.com/openai/codex/pull/20664 - Add stdio exec-server
    client transport
    - 3. https://github.com/openai/codex/pull/20665 - Make environment
    providers own default selection
    - 4. https://github.com/openai/codex/pull/20666 - Add CODEX_HOME
    environments TOML provider
    - **5. This PR:** https://github.com/openai/codex/pull/20667 - Load
    configured environments from CODEX_HOME
    
    Split from original draft: https://github.com/openai/codex/pull/20508
    
    ## Validation
    
    - `just fmt`
    - `git diff --check`
    - `bazel build --config=remote --strategy=remote
    --remote_download_toplevel
    //codex-rs/thread-manager-sample:codex-thread-manager-sample`
    - `bazel test --config=remote --strategy=remote
    --remote_download_toplevel
    //codex-rs/exec-server:exec-server-unit-tests`
    - `bazel test --config=remote --strategy=remote
    --remote_download_toplevel --test_sharding_strategy=disabled
    --test_arg=default_thread_environment_selections_use_manager_default_id
    //codex-rs/core:core-unit-tests`
    - `bazel test --config=remote --strategy=remote
    --remote_download_toplevel --test_sharding_strategy=disabled
    --test_arg=start_thread_uses_all_default_environments_from_codex_home
    //codex-rs/core:core-unit-tests`
    
    ## Documentation
    
    This activates `CODEX_HOME/environments.toml`; user-facing documentation
    should be added before this stack is treated as a documented public
    workflow.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • [codex] Generalize service tier slash commands (#21745)
    ## Why
    
    `/fast` was wired as a one-off slash command even though model metadata
    now exposes service tiers as catalog data. That meant adding another
    tier, such as a slower/cheaper tier, would require more hardcoded TUI
    plumbing instead of letting the model catalog drive the available
    commands.
    
    This change makes service-tier commands data-driven: each advertised
    `service_tiers` entry becomes a `/name` command using the catalog
    description, while the request path sends the tier `id` only when the
    selected model supports it.
    
    ## What Changed
    
    - Removed the hardcoded `/fast` slash-command variant and introduced
    dynamic service-tier command items in the composer and command popup.
    - Added toggle behavior for service-tier commands: invoking `/name`
    selects that tier, and invoking it again clears the selection.
    - Preserved the existing Fast-mode keybinding/status affordances by
    resolving the current model tier whose name is `fast`, while still
    sending the tier request value such as `priority`.
    - Persisted service-tier selections as raw request strings so non-fast
    tiers can round-trip through config.
    - Updated the Bedrock catalog entry to advertise fast support through
    `service_tiers` with `id: "priority"` and `name: "fast"`.
    - Added defensive filtering in core so unsupported selected service
    tiers are omitted from `/responses` requests.
    
    ## Validation
    
    - Added/updated coverage for dynamic service-tier slash command lookup,
    popup descriptions, composer dispatch, TUI fast toggling, and
    unsupported-tier omission in core request construction.
    - Local tests were not run per request.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Show permissions and approval mode in the TUI status line (#21677)
    Fixes #21665.
    
    ## Why
    
    The TUI status line is the right place for compact, glanceable session
    state. The original request was motivated by the need to see the active
    permission posture without opening `/permissions` or `/status`,
    especially when switching between safer and more permissive modes during
    a session.
    
    This PR intentionally separates `permissions` from `approval-mode`
    instead of combining them into one status-line item. They answer related
    but different questions: `permissions` describes the active
    sandbox/profile shape, while `approval-mode` describes how command
    approvals are handled. Keeping them separate makes each item
    independently configurable and avoids long combined labels in an already
    space-constrained status line.
    
    The tradeoff is that users who want the full permission posture in the
    status line need to opt into both items. In exchange, users can show
    only the sandbox/profile label, only the approval behavior, or both, and
    named user-defined profiles remain concise. Non-standard permission
    shapes are rendered as `Custom permissions` rather than trying to
    squeeze detailed profile contents into the status line; `/status`
    remains the fuller explanatory surface.
    
    ## What changed
    
    - Added a configurable `permissions` status-line item.
    - Added a separate `approval-mode` status-line item, with `approval` as
    an alias.
    - Render standard permission states compactly as `Read Only`,
    `Workspace`, or `Full Access`.
    - Preserve user-defined permission profile names directly in the status
    line.
    - Render unnamed non-standard permission shapes as `Custom permissions`.
    - Refresh status surfaces when `/permissions` updates the permission
    profile, approval policy, or approval reviewer.
    - Updated status-line preview snapshot coverage for the new items.
    
    ## Verification
    
    - `cargo test -p codex-tui
    status_permissions_non_default_workspace_write_uses_workspace_label`
    - `cargo test -p codex-tui
    permissions_selection_emits_history_cell_when_selection_changes`
    - `cargo insta pending-snapshots --manifest-path tui/Cargo.toml`
  • Display blended token count in status line (#21669)
    ## Why
    
    The configurable `/statusline` and terminal title can display session
    token usage. That display was using the raw total token count, which
    includes cached input tokens, so it significantly overstated the token
    usage compared with the blended token count shown elsewhere (in
    `/status` and tracked in goals). This inconsistency resulted in user
    confusion. We don't want to report cached tokens because we don't charge
    for them and they are somewhat of an implementation detail that users
    shouldn't care about.
    
    ## What changed
    
    - Use `TokenUsage::blended_total()` for the `used-tokens` status surface
    item so cached input is excluded.
    - Add a brief comment to `tokens_in_context_window()` clarifying that it
    returns raw `total_tokens`, whose meaning depends on whether the caller
    has last-turn or accumulated usage.
  • Update models.json (#19896)
    Automated update of models.json.
    
    ---------
    
    Co-authored-by: aibrahim-oai <219906144+aibrahim-oai@users.noreply.github.com>
    Co-authored-by: Ahmed Ibrahim <aibrahim@openai.com>
  • Remove skills list extra roots (#21485)
    ## Summary
    - Remove `perCwdExtraUserRoots` / `SkillsListExtraRootsForCwd` from the
    `skills/list` app-server API.
    - Drop Rust app-server and `codex-core-skills` extra-root plumbing so
    skill scans are keyed by the normal cwd/user/plugin roots only.
    - Regenerate app-server schemas and update docs/tests that only existed
    for the removed extra-roots behavior.
    
    ## Validation
    - `just write-app-server-schema`
    - `just fmt`
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-core-skills`
    - `just fix -p codex-app-server-protocol`
    - `just fix -p codex-core-skills`
    - `just fix -p codex-app-server`
    - `just fix -p codex-tui`
    
    ## Notes
    - `cargo test -p codex-app-server --test all skills_list` ran the edited
    skills-list cases, but the full filtered run ended on existing
    `skills_changed_notification_is_emitted_after_skill_change` timeout
    after a websocket `401`.
    - `cargo test -p codex-tui --lib` compiled the changed TUI callers, then
    failed two unrelated status permission tests because local
    `/etc/codex/requirements.toml` forbids `DangerFullAccess`.
    - Source-truth check found the OpenAI monorepo still has
    generated/app-server-kit mirror references to the removed field; those
    should be cleaned up when generated app-server types are synced or in a
    companion OpenAI cleanup.
  • [codex-analytics] plumb protocol-native review timing (#21434)
    ## Why
    
    We want terminal tool review analytics, but the reducer should not stamp
    review timing from its own wall clock.
    
    This PR plumbs review timing through the real protocol and app-server
    seams so downstream analytics can consume the emitter's timestamps
    directly. Guardian reviews keep their enriched `started_at` /
    `completed_at` analytics fields by deriving those legacy second-based
    values from the same protocol-native millisecond lifecycle timestamps,
    rather than sampling a separate analytics clock.
    
    ## What changed
    
    - add `started_at_ms` to user approval request payloads
    - add `started_at_ms` / `completed_at_ms` to guardian review
    notifications
    - preserve Guardian review `started_at` / `completed_at` enrichment from
    the protocol-native timing source
    - stamp typed `ServerResponse` analytics facts with app-server-observed
    `completed_at_ms`
    - thread the new timing fields through core, protocol, app-server, TUI,
    and analytics fixtures
    
    ## Verification
    
    - `cargo test -p codex-app-server outgoing_message --manifest-path
    codex-rs/Cargo.toml`
    - `cargo test -p codex-app-server-protocol guardian --manifest-path
    codex-rs/Cargo.toml`
    - `cargo test -p codex-tui guardian --manifest-path codex-rs/Cargo.toml`
    - `cargo test -p codex-analytics analytics_client_tests --manifest-path
    codex-rs/Cargo.toml`
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/21434).
    * #18748
    * __->__ #21434
    * #18747
    * #17090
    * #17089
    * #20514
  • Show plugin hooks in plugin details (#21447)
    Supersedes the abandoned #19859, rebuilt on latest `main`.
    
    # Why
    
    PR #19705 adds discovery for hooks bundled with plugins, but `/plugins`
    still only shows skills, apps, and MCP servers. This follow-up makes
    bundled hooks visible in the same plugin detail view so users can
    inspect the full plugin surface in one place.
    
    We also need `PluginHookSummary` to populate Plugin Hooks in the app;
    `hooks/list` is not enough there because plugin detail needs to show
    hooks for disabled plugins too.
    
    # What
    
    - extend `plugin/read` with `PluginHookSummary` entries for bundled
    hooks
    - summarize plugin hooks while loading plugin details
    - render a `Hooks` row in the `/plugins` detail popup
    
    <img width="3456" height="848" alt="CleanShot 2026-04-27 at 11 45 34@2x"
    src="https://github.com/user-attachments/assets/fe3a38d6-a260-4351-8513-fb04c93d725b"
    />
  • Revert state DB injection and agent graph store (#21481)
    ## Why
    
    Reverts #20689 to restore the previous optional state DB plumbing. The
    conflict resolution keeps the newer installation ID and session/thread
    identity changes that landed after #20689, while removing the mandatory
    state DB and agent graph store dependency from ThreadManager
    construction.
    
    ## What changed
    
    - Restored `Option<StateDbHandle>` through app-server, MCP server,
    prompt debug, and test entry points.
    - Removed the `codex-core` dependency on `codex-agent-graph-store` and
    reverted descendant lookup back to the existing state DB path when
    available.
    - Kept newer `installation_id` forwarding by passing it beside the
    optional DB handle.
    - Kept local thread-name updates working when the optional state DB
    handle is absent.
    
    ## Validation
    
    - `git diff --check`
    - `cargo test -p codex-thread-store`
    - `cargo test -p codex-state -p codex-rollout -p
    codex-app-server-protocol`
    - Attempted `env CARGO_INCREMENTAL=0 cargo test -p codex-core -p
    codex-app-server -p codex-app-server-client -p codex-mcp-server -p
    codex-thread-manager-sample -p codex-tui`; blocked locally by a rustc
    ICE while compiling `v8 v146.4.0` with `rustc 1.93.0 (254b59607
    2026-01-19)` on `aarch64-apple-darwin`.
  • fix(tui): clear first inline viewport render (#21450)
    ## Why
    
    The alpha TUI can render the initial trust-directory prompt with stale
    terminal text showing through spaces when startup begins below existing
    shell output. The first inline viewport transition can happen while the
    previous viewport is still empty, so the old clear path no-ops before
    Ratatui draws the prompt. Ratatui then skips blank cells because its
    previous buffer also thinks those cells are blank, leaving old terminal
    contents visible inside the prompt.
    
    ## What Changed
    
    - Clear from the new inline viewport top when the previous viewport is
    empty during a viewport transition.
    - Keep the existing clear-from-old-viewport behavior for normal viewport
    updates.
    - Add a VT100-backed regression test that pre-fills terminal contents,
    performs the first viewport clear, and verifies stale text inside the
    new viewport is removed while shell content above the viewport remains.
    
    ## How to Test
    
    1. Start Codex alpha in a terminal that already has visible shell output
    above the cursor.
    2. Use a fresh untrusted project directory so the trust-directory prompt
    appears.
    3. Confirm the prompt text renders cleanly, with spaces staying blank
    instead of showing fragments of previous shell output.
    4. As a regression check, confirm content above the inline viewport is
    still preserved in terminal scrollback.
    
    Targeted tests:
    - `cargo test -p codex-tui
    first_viewport_change_clears_from_new_viewport_when_old_viewport_is_empty
    -- --nocapture`
    - `cargo test -p codex-tui`
  • Add compact lifecycle hooks (started by vincentkoc - external contrib) (#19905)
    Based on work from Vincent K -
    https://github.com/openai/codex/pull/19060
    
    <img width="1836" height="642" alt="CleanShot 2026-04-29 at 20 47 40@2x"
    src="https://github.com/user-attachments/assets/b647bb89-65fe-40c8-80b0-7a6b7c984634"
    />
    
    ## Why
    
    Compaction rewrites the conversation context that future model turns
    receive, but hooks currently have no deterministic lifecycle point
    around that rewrite. This adds compact lifecycle hooks so users can
    audit manual and automatic compaction, surface hook messages in the UI,
    and run post-compaction follow-up without overloading tool or prompt
    hooks.
    
    ## What Changed
    
    - Added `PreCompact` and `PostCompact` hook events across hook config,
    discovery, dispatch, generated schemas, app-server notifications,
    analytics, and TUI hook rendering.
    - Added trigger matching for compact hooks with the documented `manual`
    and `auto` matcher values.
    - Wired `PreCompact` before both local and remote compaction, and
    `PostCompact` after successful local or remote compaction.
    - Kept compact hook command input to lifecycle metadata: session id,
    Codex turn id, transcript path, cwd, hook event name, model, and
    trigger.
    - Made compact stdout handling consistent with other hooks: plain stdout
    is ignored as debug output, while malformed JSON-looking stdout is
    reported as failed hook output.
    - Added integration coverage for compact hook dispatch, trigger
    matching, post-compact execution, and the audited behavior that
    `decision:"block"` does not block compaction.
    
    ## Out of Scope
    
    - Hook-specific compaction blocking is not implemented;
    `decision:"block"` and exit-code-2 blocking semantics are intentionally
    unsupported for `PreCompact`.
    - Custom compaction instructions are not exposed to compact hooks in
    this PR.
    - Compact summaries, summary character counts, and summary previews are
    not exposed to compact hooks in this PR.
    
    ## Verification
    
    - `cargo test -p codex-hooks`
    - `cargo test -p codex-core
    manual_pre_compact_block_decision_does_not_block_compaction`
    - `cargo test -p codex-app-server hooks_list`
    - `cargo test -p codex-core config_schema_matches_fixture`
    - `cargo test -p codex-tui hooks_browser`
    
    ## Docs
    
    The developer documentation for Codex hooks should be updated alongside
    this feature to document `PreCompact` and `PostCompact`, the
    `manual`/`auto` matcher values, and the compact hook payload fields.
    
    ---------
    
    Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
  • feat: Add marketplace source filtering and plugin share context (#21419)
    Adds marketplaceKinds to plugin/list for local, workspace-directory, and
    shared-with-me; omitted params keep default local plus gated global
    behavior, while explicit kinds are exact.
    
    Exposes shareContext on plugin summaries from local share mappings and
    remote workspace/shared responses, including remotePluginId and nullable
    creator metadata.
    
    Adds shared-with-me listing through /ps/plugins/workspace/shared,
    renames the workspace remote namespace to workspace-directory, and keeps
    direct remote read/share/install/update/delete paths gated by plugins
    rather than remote_plugin.
  • [codex-tui] pass thread source for tui threads (#21401)
    ## Summary
    - mark TUI-created thread starts and forks with explicit `thread_source
    = user`
    - add focused coverage for embedded and remote lifecycle request
    builders
    
    ## Why
    Thread analytics now consume an explicit thread-level source
    classification instead of inferring it from `session_source`. The TUI
    still omitted that field, so TUI-created interactive threads would
    continue to land as `null` even after the new analytics plumbing
    shipped.
    
    ## Validation
    - `cargo test -p codex-tui app_server_session --lib`
  • [codex] Dedupe fallback model metadata warnings (#21090)
    Fixes #21070.
    
    This is a small cleanup around model metadata handling for
    gateway/provider model names. It follows the report and proposed
    direction from @dkbush by keeping the fallback metadata warning useful
    without repeating it every turn, and by tightening the existing
    provider-prefix lookup path.
    
    - Track fallback metadata warning slugs in session state so each
    unresolved model warns once per session.
    - Keep warning emission outside the session-state lock and preserve the
    existing warning text.
    - Allow one-segment provider prefixes with hyphenated provider IDs,
    while preserving the multi-segment rejection behavior.
    - Add focused coverage for warning dedupe and hyphenated provider-prefix
    metadata matching.
    
    Testing:
    
    - Ran `just fmt`.
    - Ran `git diff --check`.
    - Added tests for the new warning dedupe and provider-prefix lookup
    behavior.
  • Route opted-in MCP elicitations through Guardian (#19431)
    # Motivation
    
    Browser Use origin-access prompts are MCP elicitations, not direct
    tool-call approval prompts, so they were bypassing the Guardian approval
    path. We need a generic opt-in that lets eligible MCP elicitations use
    Guardian when the current turn already routes approvals there.
    
    # Description
    
    Add a generic elicitation reviewer hook in codex-mcp and wire codex-core
    to pass a Guardian reviewer callback when creating the MCP connection
    manager. The reviewer validates explicit mcp_tool_call opt-in metadata,
    builds a Guardian MCP tool-call review request from
    server/tool/connector metadata and tool params, and maps Guardian
    approval, denial, timeout, and cancellation decisions back to MCP
    elicitation responses.
    
    The new option to trigger this in the `_meta` object is:
    ```
    "codex_request_type": "approval_request",
    ```
    
    # Testing
    
    - RUST_MIN_STACK=8388608 NEXTEST_STATUS_LEVEL=leak cargo nextest run
    --no-fail-fast --cargo-profile ci-test --test-threads 2
    - cargo clippy --tests -- -D warnings
    - cargo fmt -- --config imports_granularity=Item --check
    - cargo shear
    - pnpm run format
    - python3 .github/scripts/verify_cargo_workspace_manifests.py
    - python3 .github/scripts/verify_tui_core_boundary.py
    - python3 .github/scripts/verify_bazel_clippy_lints.py
    - git diff --check
  • fix(tui): persist ctrl-c draft via app event (#21397)
    ## Why
    
    The main branch started failing after #21351 merged because the merge
    commit kept calling `AppCommand::add_to_history` from
    `BottomPane::clear_composer_for_ctrl_c`, but main had already removed
    that helper as part of the history persistence refactor. The PR head
    passed because it was based on an older main commit where the helper
    still existed.
    
    This restores the Ctrl+C draft-stashing behavior using the current
    app-event path instead of the removed command helper.
    
    ## What Changed
    
    - Store the active `ThreadId` in `BottomPane` when history metadata is
    provided.
    - Emit `AppEvent::AppendMessageHistoryEntry` for Ctrl+C-cleared drafts.
    - Update the slash-clear regression test to assert the current history
    event shape.
    
    ## How to Test
    
    Targeted tests:
    - `cargo test -p codex-tui
    slash_clear_after_ctrl_c_keeps_stashed_draft_recallable`
    
    Broader local checks:
    - `just fix -p codex-tui`
    - `just argument-comment-lint -p codex-tui`
    - `git diff --check origin/main...HEAD`
    - `cargo test -p codex-tui` reached completion; the fixed test passed,
    and the only local failures were
    `status::tests::status_permissions_full_disk_managed_*`, blocked by this
    machine config rejecting `DangerFullAccess` via
    `/etc/codex/requirements.toml`.
  • fix(tui): keep Ctrl-C stashed drafts after /clear (#21351)
    ## Why
    
    When a user stashes a draft with Ctrl+C, then runs `/clear`, the fresh
    chat session loses the in-memory composer history that held the stashed
    draft. Pressing Up after `/clear` can then recall an older submitted
    prompt instead of the draft the user explicitly saved for later.
    
    ## What Changed
    
    - Record Ctrl+C-cleared composer text through the existing message
    history path, so it survives the fresh session created by `/clear`.
    - Keep `/clear` itself out of local slash-command recall so it does not
    sit ahead of the stashed draft.
    - Add regression coverage for the full flow: submit a prompt, stash a
    later draft with Ctrl+C, run `/clear`, then recall the stashed draft
    before the older prompt.
    
    ## How to Test
    
    1. Start Codex with `just c`.
    2. Submit a short prompt such as `ok` and wait for the turn to complete.
    3. Type a new draft, press Ctrl+C, then run `/clear`.
    4. Press Up and confirm the stashed draft is restored.
    5. Press Up again and confirm the older submitted prompt is still
    reachable after the stashed draft.
    
    Targeted tests:
    
    - `cargo test -p codex-tui
    slash_clear_after_ctrl_c_keeps_stashed_draft_recallable`
    
    Manual verification:
    
    - Reproduced the issue in tmux with `RUST_LOG=trace just c -c
    log_dir=...`: before the fix, Up after `/clear` recalled the older
    submitted prompt.
    - Re-tested the same tmux flow after the fix: Up after `/clear` restored
    the Ctrl+C-stashed draft.
  • Move message history out of core (#21278)
    ## Why
    
    Message history was implemented inside `codex-core` and surfaced through
    core protocol ops and `SessionConfiguredEvent` fields even though the
    current consumer is TUI-local prompt recall. That made core own UI
    history persistence and exposed `history_log_id` / `history_entry_count`
    through surfaces that app-server and other clients do not need.
    
    This change moves message history persistence out of core and keeps the
    recall plumbing local to the TUI.
    
    ## What changed
    
    - Added a new `codex-message-history` crate for appending, looking up,
    trimming, and reading metadata from `history.jsonl`.
    - Removed core protocol history ops/events: `AddToHistory`,
    `GetHistoryEntryRequest`, and `GetHistoryEntryResponse`.
    - Removed `history_log_id` and `history_entry_count` from
    `SessionConfiguredEvent` and updated exec/MCP/test fixtures accordingly.
    - Updated the TUI to dispatch local app events for message-history
    append/lookup and keep its persistent-history metadata in TUI session
    state.
    
    ## Validation
    
    - `cargo test -p codex-message-history -p codex-protocol`
    - `cargo test -p codex-exec event_processor_with_json_output`
    - `cargo test -p codex-mcp-server outgoing_message`
    - `cargo test -p codex-tui`
    - `just fix -p codex-message-history -p codex-protocol -p codex-core -p
    codex-tui -p codex-exec -p codex-mcp-server`
  • 2- Use string service tiers in session protocol (#20971)
    ## Summary
    - break service tier session/op/app-server protocol fields from the
    closed enum to string tier ids
    - send the service tier string directly through model requests, prewarm,
    compaction, memories, and TUI/app-server turn starts
    - regenerate app-server protocol JSON/TypeScript schemas, removing the
    standalone ServiceTier TS enum
    
    ## Verification
    - just fmt
    - cargo check -p codex-core -p codex-app-server -p codex-tui
    - just write-app-server-schema
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • feat(app-server): move v2 sessionId onto Thread (#21336)
    ## Why
    
    `session_id` and `thread_id` are separate identities after #20437, but
    app-server only surfaced `sessionId` on the `thread/start`,
    `thread/resume`, and `thread/fork` response envelopes. Other
    thread-bearing surfaces such as `thread/list`, `thread/read`,
    `thread/started`, `thread/rollback`, `thread/metadata/update`, and
    `thread/unarchive` either lacked the grouping key or forced clients to
    special-case those three responses.
    
    Making `sessionId` part of the reusable `Thread` payload gives every v2
    API surface one place to expose session-tree identity.
    
    ## Mental model
      1. thread.sessionId lives on `Thread`
    2. It is a view/runtime identity for the current live session tree, not
    durable stored lineage metadata
    3. When app-server has a live loaded thread, it copies the real value
    from core’s session_configured.session_id
    4. When it only has stored/unloaded data, it falls back to
    thread.sessionId = thread.id
    
    ## What changed
    
    - Added `sessionId` to the v2
    [`Thread`](https://github.com/openai/codex/blob/8fc9e9b4cf81b6f61d432e71f1eb266f6f104b63/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs#L105-L109).
    - Removed the duplicate top-level `sessionId` fields from
    `thread/start`, `thread/resume`, and `thread/fork`; clients should now
    read `response.thread.sessionId`.
    - Populated `thread.sessionId` when building live thread responses,
    replaying loaded threads, and returning stored-thread summaries so the
    field is present across start, resume, fork, list, read, rollback,
    metadata-update, unarchive, and `thread/started` paths. See
    [`load_thread_from_resume_source_or_send_internal`](https://github.com/openai/codex/blob/8fc9e9b4cf81b6f61d432e71f1eb266f6f104b63/codex-rs/app-server/src/request_processors/thread_processor.rs#L2824-L2918)
    and
    [`thread_from_stored_thread`](https://github.com/openai/codex/blob/8fc9e9b4cf81b6f61d432e71f1eb266f6f104b63/codex-rs/app-server/src/request_processors/thread_processor.rs#L3671-L3719).
    - Preserved the stored-thread fallback: if a thread has not been loaded
    into a live session tree yet, `thread.sessionId` falls back to
    `thread.id`; once the thread is live again, the field reports the active
    session tree root.
    - Regenerated the JSON/TypeScript schemas and updated the app-server
    README examples to show
    [`thread.sessionId`](https://github.com/openai/codex/blob/8fc9e9b4cf81b6f61d432e71f1eb266f6f104b63/codex-rs/app-server/README.md#L306-L310)
    on the thread object.
  • feat: add session_id (#20437)
    ## Summary
    
    Related to
    https://openai.slack.com/archives/C095U48JNL9/p1777537279707449
    TLDR:
    We update the meaning of session ids and thread ids:
    * thread_id stays as now
    * session_id become a shared id between every thread under a /root
    thread (i.e. every sub-agent share the same session id)
    
    This PR introduces an explicit `SessionId` and threads it through the
    protocol/client boundary so `session_id` and `thread_id` can diverge
    when they need to, while preserving compatibility for older serialized
    `session_configured` events.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Support Codex Apps auth elicitations (#19193)
    ## Summary
    
    - request URL-mode MCP elicitations when Codex Apps tool calls fail with
    connector auth metadata
    - route Codex Apps auth URL elicitations into the TUI app-link flow
    
    ## Test plan
    
    - `just fmt`
    - `cargo test -p codex-core mcp_tool_call::tests`
    - `cargo test -p codex-mcp`
    - `cargo test -p codex-tui bottom_pane::app_link_view::tests`
    - `just fix -p codex-core`
    - `just fix -p codex-mcp`
    - `just fix -p codex-tui`
    
    Also attempted broader local runs:
    
    - `cargo test -p codex-core` fails in unrelated
    config/request-permission/proxy-sensitive tests under the current Codex
    Desktop environment.
    - `cargo test -p codex-tui` fails in unrelated status
    snapshots/trust-default tests because the ambient environment renders
    workspace-write/network permission defaults.
  • [codex-analytics] rework thread_source for thread analytics (#20949)
    ## Summary
    - make `thread_source` an explicit optional thread-level field on
    `thread/start`, `thread/fork`, and returned thread payloads
    - persist `thread_source` in rollout/session metadata so resumed live
    threads retain the original value
    - replace the old best-effort `session_source` -> `thread_source`
    mapping with an explicit caller-supplied analytics classification
    
    ## Why
    Before this change, analytics `thread_source` was populated by a
    best-effort mapping from `session_source`. `session_source` describes
    the runtime/client surface, not the actual thread-level origin, so that
    projection was not accurate enough to distinguish cases such as `user`,
    `subagent`, `memory_consolidation`, and future thread origins reliably.
    
    Making `thread_source` explicit keeps one thread-level analytics field
    while letting callers provide the real classification directly instead
    of recovering it indirectly from `session_source`.
    
    ## Impact
    For new analytics events, `thread_source` now reflects the explicit
    thread-level classification supplied by the caller rather than an
    inferred value derived from `session_source`. Existing protocol fields
    remain optional; callers that omit `threadSource` now produce `null`
    instead of a best-effort inferred value.
    
    ## Validation
    - `just write-app-server-schema`
    - `cargo test -p codex-analytics -p codex-core -p
    codex-app-server-protocol --no-run`
    - `cargo test -p codex-app-server-protocol
    generated_ts_optional_nullable_fields_only_in_params`
    - `cargo test -p codex-analytics
    thread_initialized_event_serializes_expected_shape`
    - `cargo test -p codex-core
    resume_stopped_thread_from_rollout_preserves_thread_source`
  • Expose plugin manifest keywords in app server (#21271)
    ## Summary
    - Add plugin manifest keywords to core plugin marketplace/detail models
    - Expose keywords on app-server v2 PluginSummary and generated
    schema/types
    - Populate keywords in plugin/list and plugin/read responses for local
    plugins
    
    Depends on https://github.com/openai/openai/pull/891087
    
    ## Validation
    - just fmt
    - just write-app-server-schema
    - cargo test -p codex-app-server-protocol
    - cargo test -p codex-core-plugins
    - cargo test -p codex-app-server
    plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_load
    - cargo test -p codex-app-server
    plugin_read_returns_plugin_details_with_bundle_contents
  • fix build (#21261)
    I believe a merge race in https://github.com/openai/codex/pull/20689
    broke the build, so this is a quick fix.
    
    `cargo check --tests` passed locally.
  • Inject state DB, agent graph store (#20689)
    ## Why
    
    We want the agent graph store to be passed down the stack as a real
    dependency, the same way we already treat the thread store.
    
    This will let us inject the agent graph store as a real dependency and
    support implementations other than the local SQLite-backed one. Right
    now most code instantiates a state DB and an agent graph store
    just-in-time. Ideally, we would not depend on the state DB directly but
    only read through the higher-level interfaces.
    
    This change makes the dependency boundaries explicit and moves state DB
    initialization to process bootstrap instead of hiding it inside local
    store implementations.
    
    ## What changed
    
    - `ThreadManager` now requires a `StateDbHandle` and an
    `AgentGraphStore` at construction time instead of treating them as
    optional internals.
    - The local store constructors no longer lazily initialize SQLite.
    Callers now initialize the state DB once per process and use that shared
    handle to build:
      - `LocalThreadStore`
      - `LocalAgentGraphStore`
    - App bootstraps (`app-server`, `mcp-server`, `prompt_debug`, and the
    thread-manager sample) now initialize the state DB up front and inject
    the resulting handle down the stack.
    - `app-server` now consistently uses its process-scoped state DB handle
    instead of reopening SQLite or trying to recover it from loaded threads.
    - Device-key storage now reuses the shared state DB handle instead of
    maintaining its own lazy opener.
    - The thread archive / descendant traversal paths now use the injected
    `AgentGraphStore` instead of reaching through local
    thread-store-specific state.
    
    ## Verification
    
    - `cargo check -p codex-core -p codex-thread-store -p codex-app-server
    -p codex-mcp-server -p codex-thread-manager-sample --tests`
    - `cargo test -p codex-thread-store`
    - `cargo test -p codex-core
    thread_manager_accepts_separate_agent_graph_store_and_thread_store --
    --nocapture`
    - `cargo test -p codex-app-server
    thread_archive_archives_spawned_descendants -- --nocapture`
  • [codex] fix TUI turn items view fixtures (#21243)
    ## Summary
    
    Adds the required `items_view` field to the three session picker `Turn`
    test fixtures that populate full turn item lists.
    
    ## Root Cause
    
    `#21063` added `Turn.items_view` to the app-server protocol type. The
    later session picker merge added three test-only
    `codex_app_server_protocol::Turn` literals without the new field, which
    broke Bazel compilation on `main` with `E0063: missing field
    items_view`.
    
    ## Validation
    
    - `just fmt`
    - `cargo test -p codex-tui resume_picker --no-fail-fast`
    - `just argument-comment-lint`
    
    I also ran `cargo test -p codex-tui`; it compiled and ran the suite, but
    this local machine failed two pre-existing status permission-profile
    tests because `/etc/codex/requirements.toml` disallows
    `DangerFullAccess`.
  • feat(tui): redesign session picker (#20065)
    ## Why
    
    The resume/fork picker is becoming the main way users recover previous
    work, but the old fixed table made sessions hard to scan once thread
    names, branches, working directories, and timestamps all mattered. This
    redesign makes the picker denser by default, easier to search, and safer
    to inspect before resuming or forking.
    
    <table>
    <tr>
    <td>
    <img width="1660" height="1103" alt="CleanShot 2026-05-03 at 12 34 10"
    src="https://github.com/user-attachments/assets/313ede1d-1da4-4863-acd2-56b3e27e9703"
    />
    </td>
    <td>
    <img width="1662" height="1100" alt="CleanShot 2026-05-03 at 12 34 15"
    src="https://github.com/user-attachments/assets/cfde7d5c-bab0-4994-a807-254e53f344ea"
    />
    </td>
    </tr>
    <tr>
    <td>
    <img width="1664" height="1107" alt="CleanShot 2026-05-03 at 12 39 22"
    src="https://github.com/user-attachments/assets/e1ee58ca-4dc5-4a35-ae0f-47562da3974c"
    />
    </td>
    <td>
    <img width="1662" height="1100" alt="CleanShot 2026-05-03 at 12 35 09"
    src="https://github.com/user-attachments/assets/9c888072-eedf-4f45-985c-0c14df28bcc7"
    />
    </td>
    </tr>
    </table>
    
    ## What Changed
    
    - Replaces the old session table with responsive session rows that
    prioritize the session name or preview, then show timestamp, cwd, and
    branch metadata.
    - Makes dense view the default while keeping comfortable view available
    through `Ctrl+O`.
    - Persists the picker view preference in `[tui].session_picker_view`,
    including active profile-scoped config.
    - Adds sort/filter controls for updated time, created time, cwd, and all
    sessions.
    - Expands search matching across session name, preview, thread id,
    branch, and cwd.
    - Makes `Esc` safer in search mode: it clears an active query before
    starting a new session.
    - Adds lazy transcript inspection:
      - `Space` expands recent transcript context inline.
      - `Ctrl+T` opens a transcript overlay.
      - raw reasoning visibility follows `show_raw_agent_reasoning`.
    - Keeps remote cwd filtering server-side for remote app-server sessions
    so local path normalization does not incorrectly hide remote results.
    - Updates snapshots and config schema for the new picker states and
    config option.
    
    ## How to Test
    
    1. Start Codex in a repo with several saved sessions.
    2. Press `Ctrl+R` / resume picker entry point.
    3. Confirm the picker opens in dense mode and shows session name or
    preview, timestamp, cwd, and branch metadata.
    4. Press `Ctrl+O` and confirm it switches between dense and comfortable
    views.
    5. Restart Codex and confirm the selected view persists.
    6. Type a query that matches a branch, cwd, thread id, or session name;
    confirm matching sessions appear.
    7. Press `Esc` while the query is non-empty and confirm it clears search
    instead of starting a new session.
    8. Select a session and press `Space`; confirm recent transcript context
    expands inline.
    9. Press `Ctrl+T`; confirm the transcript overlay opens and respects
    raw-reasoning visibility settings.
    
    Targeted tests:
    - `cargo test -p codex-tui resume_picker --no-fail-fast`
    - `cargo test -p codex-core
    runtime_config_resolves_session_picker_view_default_and_override`
    - `cargo test -p codex-core profile_tui_rejects_unsupported_settings`
    - `cargo check -p codex-thread-manager-sample`
    - `cargo insta pending-snapshots`
  • feat(tui): route /diff through workspace commands (#21001)
    Stacked on #20892.
    
    ## Why
    
    #20892 adds the TUI workspace command abstraction so branch status
    metadata can run through app-server instead of assuming the CLI process
    has the active workspace locally. `/diff` still used direct local
    process execution, which means remote app-server sessions could compute
    the diff against the wrong machine or fail to see the active workspace
    at all.
    
    This PR moves `/diff` onto that same app-server-backed command path so
    Git runs wherever the active workspace lives.
    
    ## What Changed
    
    - Route `/diff` through the TUI `WorkspaceCommandExecutor` using the
    active chat cwd.
    - Replace direct `tokio::process::Command` usage in `get_git_diff` with
    argv-based workspace command requests.
    - Preserve the existing `/diff` behavior: tracked diff output, untracked
    file diffs, treating Git diff exit code `1` as success, and showing the
    existing non-git-repository message.
    - Extend `WorkspaceCommand` with caller-set timeouts and an explicit
    uncapped-output opt-out. Metadata probes remain capped by default;
    `/diff` opts out because its full output is the user-visible payload.
    
    ## How to Test
    
    Manual reviewer path:
    
    1. Start the Codex TUI from a Git worktree with one tracked file change
    and one untracked file.
    2. Run `/diff`.
    3. Confirm the rendered diff includes both the tracked diff and the
    untracked file diff.
    4. Start the TUI outside a Git worktree, or switch to a non-git cwd,
    then run `/diff`.
    5. Confirm it shows the existing `/diff` not-inside-a-git-repository
    message.
    
    Targeted tests run:
    
    - `cargo test -p codex-tui get_git_diff -- --nocapture`
    - `cargo test -p codex-tui branch_summary -- --nocapture`
    - `cargo test -p codex-tui`
  • add turn items view to app-server turns (#21063)
    ## Why
    
    `Turn.items` currently overloads an empty array to mean either that no
    items exist or that the server intentionally did not load them for this
    response. That ambiguity blocks future lazy-loading work where clients
    need to distinguish unloaded, summary, and fully hydrated turn payloads.
    
    ## What changed
    
    - add a new `TurnItemsView` enum with `notLoaded`, `summary`, and `full`
    variants
    - add required `itemsView` metadata to app-server `Turn` payloads
    - mark reconstructed persisted history as `full` and live shell-style
    turn payloads as `notLoaded`
    - keep current `thread/turns/list` behavior unchanged and document that
    it still returns `full` turns today
    - regenerate the JSON and TypeScript protocol fixtures
    
    ## Verification
    
    - `just write-app-server-schema`
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-app-server thread_read_can_include_turns`
    - `cargo test -p codex-app-server
    thread_turns_list_can_page_backward_and_forward`
    - `cargo test -p codex-app-server
    thread_resume_rejects_history_when_thread_is_running`
    - `just fix -p codex-app-server-protocol`
    - `just fix -p codex-app-server`
    - `just fmt`
  • hook trust metadata and enforcement (#20321)
    # Why
    
    We want shared hook trust that both the app and the TUI can build on,
    but the metadata is only useful if runtime behavior agrees with it. This
    PR adds a single backend trust model for hooks so unmanaged hooks cannot
    run until the current definition has been reviewed, while managed hooks
    remain runnable and non-configurable.
    
    # What
    
    - persist `trusted_hash` alongside hook state in `config.toml`
    - expose `currentHash` and derived `trustStatus` through `hooks/list`
    - derive trust from normalized hook definitions so equivalent hooks from
    `config.toml` and `hooks.json` share the same trust identity
    - gate unmanaged hooks on trust before they enter the runnable handler
    set
    
    # Reviewer Notes
    
    - key file to review is `codex-rs/hooks/src/engine/discovery.rs`
    - the only **core** change is schema related
  • feat(tui): add raw scrollback mode (#20819)
    ## Why
    
    Granular copy is particularly difficult with the current output. Part of
    it was solved with the introduction of the `/copy` command but when you
    only need to copy parts of a response, you still encounter some issues:
    
    - When you copy a paragraph, the result is a sequence of separate lines
    instead of one correctly joined paragraph.
    - When a word wraps, part of it stays on the original line and the rest
    appears at the start of the next line.
    - When you copy a long command, extra line breaks are often inserted,
    and command arguments can be split across multiple lines.
    
    
    https://github.com/user-attachments/assets/0ef85c84-9363-4aad-b43a-15fce062a443
    
    ## Solution
    
    Now that we own the scrollback and we re-create it when we resize, we
    have the opportunity of toggling between the raw text and the rich text
    we see today.
    
    - Add TUI raw scrollback mode with `tui.raw_output_mode`, `/raw
    [on|off]`, and the configurable `tui.keymap.global.toggle_raw_output`
    action.
    - Render transcript cells through rich/raw-aware paths so raw mode
    preserves source text and lets the terminal soft-wrap selection-friendly
    output.
    - Bind raw-mode toggle to `alt-r` by default, with the keybinding path
    toggling silently while `/raw` continues to emit confirmation messages.
    
    ## Related Issues
    
    Likely addressed by raw mode:
    
    - #12200: clean copy for multiline and soft-wrapped output. Raw mode
    removes Codex-inserted wrapping/indentation and lets the terminal
    soft-wrap logical lines.
    - #9252: command suggestions gain unwanted leading spaces when copied.
    Raw mode renders transcript text without the rich-mode left
    padding/gutter.
    - #8258: prompt output is hard to copy because of leading indentation.
    Raw mode renders user/source-backed transcript text without that
    decorative indentation.
    
    Partially or conditionally addressed:
    
    - #2880: copy/export message as Markdown. Raw mode exposes raw Markdown
    for terminal selection, but this PR does not add a dedicated
    export/copy-message command.
    - #19820: mouse drag selection + copy in the TUI. Raw mode improves
    terminal-native selection of output/history text, but this PR does not
    implement in-TUI mouse selection, highlighting, auto-copy, or composer
    selection.
    - #18979: copied content is divided into two parts. This should improve
    cases caused by Codex-inserted wraps/padding in rendered output; if the
    report is about pasting into the composer/input path, that remains
    outside this PR.
    
    ## Validation
    
    - `just write-config-schema`
    - `just fmt`
    - `cargo test -p codex-config`
    - `cargo test -p codex-tui`
    - `just fix -p codex-tui`
    - `just argument-comment-lint`
    - `cargo test -p codex-tui
    raw_output_mode_can_change_without_inserting_notice -- --nocapture`
    - `cargo test -p codex-tui
    raw_slash_command_toggles_and_accepts_on_off_args -- --nocapture`
    - `cargo test -p codex-tui raw_output_toggle -- --nocapture`
    - `git diff --check`
    - `cargo insta pending-snapshots`
  • fix(tui): external editor expansion for same-size large pastes (#21190)
    ## Why
    
    We found this while reviewing #21091, but confirmed it is not introduced
    by that PR: the order-sensitive `current_text_with_pending()`
    replacement loop already existed, and `main` already allowed active
    same-size large pastes to use prefix-overlapping labels such as `[Pasted
    Content N chars]` and `[Pasted Content N chars] #2`.
    
    #21091 fixes placeholder numbering after a draft is cleared, so a fresh
    same-size paste can reuse the base label. This PR fixes a different
    path: when a draft already contains multiple active same-size large
    pastes, the placeholders can overlap by prefix, for example `[Pasted
    Content N chars]` and `[Pasted Content N chars] #2`.
    
    That overlap breaks `current_text_with_pending()` when the composer
    materializes the draft text for the external editor. Replacing the base
    placeholder first can partially rewrite the `#2` placeholder, leaving
    the external editor seeded with corrupted text instead of both paste
    payloads.
    
    | Before | After |
    |---|---|
    | <img width="1230" height="1008" alt="CleanShot 2026-05-05 at 10 18 09"
    src="https://github.com/user-attachments/assets/88a2936c-cf00-4adc-8567-8fd8f398b4a8"
    /> | <img width="1230" height="1008" alt="CleanShot 2026-05-05 at 10 20
    31"
    src="https://github.com/user-attachments/assets/119cff52-43c8-432a-9367-418d82f4ed82"
    /> |
    | <img width="1230" height="1008" alt="CleanShot 2026-05-05 at 10 18 57"
    src="https://github.com/user-attachments/assets/026031bb-839b-4252-a0fd-9ba9616435fe"
    /> | <img width="1230" height="1008" alt="CleanShot 2026-05-05 at 10 21
    31"
    src="https://github.com/user-attachments/assets/8cb6f2c8-3a5d-411b-8623-dca666ee3c08"
    /> |
    
    ## What Changed
    
    - Changed `current_text_with_pending()` to expand pending pastes through
    the existing element-range based `expand_pending_pastes()` helper
    instead of global string replacement.
    - Added a regression test with two different same-length large pastes to
    ensure both overlapping placeholders expand to their original payloads.
    
    ## How to Test
    
    1. Start Codex TUI.
    2. Paste a large string, for example 1004 `A` characters.
    ```shell
    perl -e 'print "A" x 1004' | pbcopy
    ```
    3. Paste a second large string with the same length, for example 1004
    `B` characters.
    ```shell
    perl -e 'print "B" x 1004' | pbcopy
    ```
    4. Open the external editor from the composer.
    5. Confirm the editor is seeded with the full `A...` payload followed by
    the full `B...` payload, with no literal `#2` left behind.
    
    Targeted tests:
    - `cargo test -p codex-tui
    current_text_with_pending_expands_overlapping_placeholders`
    - `just argument-comment-lint-from-source -p codex-tui`
    
    I also ran `cargo test -p codex-tui`; it reached the full crate suite
    but failed two unrelated local status tests because this machine's
    `/etc/codex/requirements.toml` rejects `DangerFullAccess`.
  • [codex] Fix fork --last cwd filtering (#21089)
    Fixes #20945.
    
    This keeps `codex fork --last` aligned with the neighboring
    latest-session lookup flows. The local fork path now uses the same
    cwd-scope helper as `resume --last`, which is also a small code cleanup
    around how this selection logic is shared.
    
    Credit to @chanwooyang1 for the report and for pointing out the narrow
    fix direction.
    
    What changed:
    - Route `fork --last` through the shared latest-session cwd filter.
    - Preserve `--all` as the explicit opt-in for global latest-session
    selection.
    - Keep remote cwd override behavior unchanged.
    - Add focused coverage for local default, `--all`, and remote override
    filter semantics.
    
    Validation:
    - Ran `just fmt`.
    - Ran `git diff --check`.
    - Reviewed the `fork --last`, `resume --last`, and fork picker selection
    paths against the issue report.
  • [codex] Fix TUI large paste placeholder numbering after Ctrl+C (#21091)
    Fixes #19940.
    
    Large-paste placeholder numbering was backed by a per-size counter, so
    clearing a draft with `Ctrl+C` left numbering state behind even though
    the active pending paste state was gone. This updates the composer to
    derive the next placeholder suffix from active pending pastes instead,
    which keeps simultaneous same-size pastes distinct while letting fresh
    drafts reuse the base label. This is also a small code cleanup: pending
    paste state is now the source of truth instead of maintaining a separate
    counter.
    
    Credit to @Sungyoun-Kim for the issue report, root-cause notes, and fork
    with the proposed fix, and to @charley-oai for the earlier related
    #10032 proposal.
    
    Changes:
    - Remove the monotonic large-paste counter from the composer.
    - Compute suffixes from currently active pending paste placeholders.
    - Document large-paste placeholder behavior in the composer module docs.
    - Add regression coverage for `Ctrl+C` clearing and deletion/reset
    behavior.
    
    Testing:
    - `just fmt`
    - `git diff --check`
  • Validate /goal objective length in TUI (#20746)
    ## Why
    
    Long `/goal` definitions currently reach lower-level goal validation and
    can produce an opaque failure. This bug was reported by a user. Pasted
    instruction blocks are especially confusing because the composer can
    still contain a paste placeholder before expansion, which may otherwise
    fall into the generic prompt-size error path.
    
    There was also a related paste edge case where `/goal ` followed by a
    multiline block whose first pasted line was blank looked like a bare
    `/goal` command. That showed the goal usage/summary instead of setting
    the pasted objective.
    
    ## What Changed
    
    This adds TUI-side preflight validation for `/goal <objective>` using
    the shared `MAX_THREAD_GOAL_OBJECTIVE_CHARS` limit. Oversized typed,
    queued, and pasted goal objectives now fail locally with a goal-specific
    message that recommends putting longer instructions in a file and
    referencing that file from the goal.
    
    The TUI now also lets inline-argument slash commands consume later-line
    arguments before treating the first line as a bare command, so `/goal `
    followed by blank lines and then objective text sets the goal instead of
    opening the bare `/goal` flow.
    
    ## Manual Testing
    
    1. Start the TUI with goals enabled and an active session.
    2. Submit `/goal ` followed by exactly 4,000 objective characters. It
    should continue through the normal goal-setting path.
    3. Submit `/goal ` followed by 4,001 objective characters. It should not
    set a goal, and should show `Goal objective is too long: 4,001
    characters. Limit: 4,000 characters.` followed by the guidance to put
    longer instructions in a file and reference that file from the goal.
    4. Type `/goal `, paste a large block that becomes a `[Pasted Content
    ... chars]` placeholder, then submit. It should validate the expanded
    pasted text and show the goal-specific file guidance rather than the
    generic prompt-size error.
    5. Type `/goal `, paste a multiline block whose first line is blank,
    then submit. It should set the objective from the non-blank pasted
    content instead of showing `Usage: /goal <objective>` or the bare goal
    summary.
    6. While a turn is running, queue an oversized `/goal` command. When the
    queue drains, it should show the same goal-specific error and should not
    emit a goal-setting request.
  • fix(tui): make /copy work inside tmux without passthrough (#20207)
    ## Summary
    - prefer tmux's native clipboard integration for `/copy` when running
    inside tmux
    - fall back to OSC 52 when tmux clipboard copy is unavailable
    - add coverage for tmux-preferred, fallback, and combined-failure paths
    
    ## Why
    Inside tmux, `/copy` previously relied on DCS-wrapped OSC 52 when `TMUX`
    was set. That only reaches the outer terminal when tmux passthrough is
    enabled, so Codex could report success even though the system clipboard
    never changed.
    
    ## User impact
    `/copy` now works inside tmux even when `allow-passthrough` is off, as
    long as tmux clipboard integration is available. If tmux cannot handle
    the copy, Codex still keeps the existing OSC 52 fallback path.
    
    ## Validation
    - `cargo test -p codex-tui`
    - `just fmt`
    - `just fix -p codex-tui`
    - `just argument-comment-lint`
    - manually verified `/copy` inside tmux with `allow-passthrough off`
    
    Fixes #19926
  • 1- Add model service tiers metadata (#20969)
    ## Why
    
    The model list needs to carry display-ready service tier metadata so
    clients can render tier choices with stable IDs, names, and
    descriptions. A raw speed-tier string list is not enough for richer UI
    copy or future tier labels.
    
    ## What changed
    
    - Added `ModelServiceTier` to shared model metadata with string `id`,
    `name`, and `description` fields.
    - Added `service_tiers` to `ModelInfo` and `ModelPreset`, preserving
    empty defaults for older cached model payloads.
    - Exposed `serviceTiers` on app-server v2 `Model` responses and threaded
    it through TUI app-server model conversion.
    - Marked legacy `additional_speed_tiers` / `additionalSpeedTiers`
    metadata as deprecated in source and generated schema output.
    - Regenerated app-server protocol JSON schema and TypeScript fixtures,
    including `ModelServiceTier.ts`.
    
    ## Verification
    
    - Ran `just write-app-server-schema`.
    - Did not run local tests per repo instruction; relying on PR CI.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Add plugin ID to skill analytics (#20923)
    ## Summary
    - thread plugin skill roots through the skills loader with their plugin
    ID
    - store plugin ID on loaded skill metadata for plugin-provided skills
    - include plugin ID on skill invocation analytics events
    
    ## Test plan
    - cargo check -p codex-core-skills
    - cargo check -p codex-core -p codex-core-plugins -p codex-analytics
    - cargo check -p codex-tui
    - cargo check -p codex-plugin -p codex-core -p codex-core-plugins -p
    codex-analytics
    - cargo check -p codex-app-server
    - cargo test -p codex-analytics
    - HOME=/private/tmp/codex-empty-home cargo test -p codex-core-skills
    - just fix -p codex-core-skills
    - just fix -p codex-analytics
    - just fix -p codex-core-plugins
    - just fix -p codex-core
    - just fmt
    - git diff --check
  • [codex] Add unsandboxed process exec API (#19040)
    ## Why
    
    App-server clients sometimes need argv-based local process execution
    while sandbox policy is controlled outside Codex. Those environments can
    reject sandbox-disabling paths before a command ever starts, even when
    the caller intentionally wants unsandboxed execution.
    
    This PR adds a distinct `process/*` API for that use case instead of
    extending `command/exec` with another sandbox-disabling shape. Keeping
    the new surface separate also makes the future removal of `command/exec`
    simpler: clients that need explicit process lifecycle control can move
    to the newer handle-based API without depending on `command/exec`
    business logic.
    
    ## What changed
    
    - Added v2 process lifecycle methods: `process/spawn`,
    `process/writeStdin`, `process/resizePty`, and `process/kill`.
    - Added process notifications: `process/outputDelta` for streamed
    stdout/stderr chunks and `process/exited` for final exit status and
    buffered output.
    - Made `process/spawn` intentionally unsandboxed and omitted
    sandbox-selection fields such as `sandboxPolicy` and
    `permissionProfile`.
    - Added client-supplied, connection-scoped `processHandle` values for
    follow-up control requests and notification routing.
    - Supported cwd, environment overrides, PTY mode and size, stdin
    streaming, stdout/stderr streaming, per-stream output caps, and timeout
    controls.
    - Killed active process sessions when the originating app-server
    connection closes.
    - Wired the implementation through the modular `request_processors/`
    app-server layout, with process-handle request serialization for
    follow-up control calls.
    - Updated generated JSON/TypeScript schema fixtures and documented the
    new API in `codex-rs/app-server/README.md`.
    - Added v2 app-server integration coverage in
    `codex-rs/app-server/tests/suite/v2/process_exec.rs` for spawn
    acknowledgement before exit, buffered output caps, and process
    termination.
    
    ## Verification
    
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-app-server`
    
    ---------
    
    Co-authored-by: Owen Lin <owen@openai.com>