Commit Graph

29 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.
  • 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
    ```
  • [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>
  • 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(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`
  • 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`
  • feat(tui): add PR summary statusline items (#20892)
    ## Why?
    
    The Codex App already exposes branch and PR context in its
    branch-details UI. This brings the same context into the CLI footer as
    opt-in statusline items, so users can choose the extra signal without
    making the default footer busier.
    
    ## What?
    
    Add optional `pull-request-number` and `branch-changes` items to the
    configurable TUI status line.
    
    - `pull-request-number` shows the open PR for the current checkout and
    renders as a clickable terminal hyperlink when OSC 8 links are
    supported.
    - `branch-changes` shows committed additions/deletions against the
    repository default branch, or `No changes` when the branch has no
    committed diff.
    
    <img width="1257" height="261" alt="CleanShot 2026-05-03 at 20 44 15"
    src="https://github.com/user-attachments/assets/10b4380b-c3e9-4729-9ee1-3f742068fa47"
    />
    
    ## Architecture
    
    This follows the same client/app-server split as the Codex App: the TUI
    owns presentation, caching, and optional rendering, while
    workspace-sensitive `git` and `gh` discovery runs through app-server.
    
    The new TUI-local `workspace_command` layer sends bounded,
    non-interactive `command/exec` requests to the active app-server. That
    makes the implementation remote-friendly: the TUI does not decide
    whether commands run in an embedded local workspace or a remote
    workspace, and it does not bypass app-server sandbox or permission
    policy.
    
    The branch summary logic stays internal to `codex-tui` because this PR
    only needs TUI statusline behavior. The command boundary is still
    isolated behind `WorkspaceCommandExecutor`, so the lookup code can be
    lifted or reused later without changing statusline rendering.
    
    ## How?
    
    - Add a TUI `WorkspaceCommandExecutor` abstraction backed by app-server
    `command/exec`.
    - Add branch summary probes for:
      - current branch name,
      - open PR metadata,
      - committed branch diff stats against the default branch.
    - Prefer remote-tracking default branch refs for diff stats, avoiding
    stale or absent local `main` branches.
    - Resolve PRs with `gh pr view` first, then fall back to
    commit-associated PR lookup across parent/fork repos.
    - Add `/statusline` picker entries, preview values, rendering, and OSC 8
    clickable PR links.
    - Keep all probes best-effort so missing `git`, missing `gh`, auth
    failures, or non-git directories hide optional items instead of
    surfacing footer errors.
    
    ## Validation
    
    - `cargo test -p codex-tui branch_summary -- --nocapture`
    - Snapshot coverage for the `/statusline` preview/setup rendering paths
    - Hyperlink rendering coverage for clickable PR statusline cells
  • state: pass state db handles through consumers (#20561)
    ## Why
    
    SQLite state was still being opened from consumer paths, including lazy
    `OnceCell`-backed thread-store call sites. That let one process
    construct multiple state DB connections for the same Codex home, which
    makes SQLite lock contention and `database is locked` failures much
    easier to hit.
    
    State DB lifetime should be chosen by main-like entrypoints and tests,
    then passed through explicitly. Consumers should use the supplied
    `Option<StateDbHandle>` or `StateDbHandle` and keep their existing
    filesystem fallback or error behavior when no handle is available.
    
    The startup path also needs to keep the rollout crate in charge of
    SQLite state initialization. Opening `codex_state::StateRuntime`
    directly bypasses rollout metadata backfill, so entrypoints should
    initialize through `codex_rollout::state_db` and receive a handle only
    after required rollout backfills have completed.
    
    ## What Changed
    
    - Initialize the state DB in main-like entrypoints for CLI, TUI,
    app-server, exec, MCP server, and the thread-manager sample.
    - Pass `Option<StateDbHandle>` through `ThreadManager`,
    `LocalThreadStore`, app-server processors, TUI app wiring, rollout
    listing/recording, personality migration, shell snapshot cleanup,
    session-name lookup, and memory/device-key consumers.
    - Remove the lazy local state DB wrapper from the thread store so
    non-test consumers use only the supplied handle or their existing
    fallback path.
    - Make `codex_rollout::state_db::init` the local state startup path: it
    opens/migrates SQLite, runs rollout metadata backfill when needed, waits
    for concurrent backfill workers up to a bounded timeout, verifies
    completion, and then returns the initialized handle.
    - Keep optional/non-owning SQLite helpers, such as remote TUI local
    reads, as open-only paths that do not run startup backfill.
    - Switch app-server startup from direct
    `codex_state::StateRuntime::init` to the rollout state initializer so
    app-server cannot skip rollout backfill.
    - Collapse split rollout lookup/list APIs so callers use the normal
    methods with an optional state handle instead of `_with_state_db`
    variants.
    - Restore `getConversationSummary(ThreadId)` to delegate through
    `ThreadStore::read_thread` instead of a LocalThreadStore-specific
    rollout path special case.
    - Keep DB-backed rollout path lookup keyed on the DB row and file
    existence, without imposing the filesystem filename convention on
    existing DB rows.
    - Verify readable DB-backed rollout paths against `session_meta.id`
    before returning them, so a stale SQLite row that points at another
    thread's JSONL falls back to filesystem search and read-repairs the DB
    row.
    - Keep `debug prompt-input` filesystem-only so a one-off debug command
    does not initialize or backfill SQLite state just to print prompt input.
    - Keep goal-session test Codex homes alive only in the goal-specific
    helper, rather than leaking tempdirs from the shared session test
    helper.
    - Update tests and call sites to pass explicit state handles where DB
    behavior is expected and explicit `None` where filesystem-only behavior
    is intended.
    
    ## Validation
    
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo check -p
    codex-rollout -p codex-thread-store -p codex-app-server -p codex-core -p
    codex-tui -p codex-exec -p codex-cli --tests`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p
    codex-rollout state_db_`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p
    codex-rollout find_thread_path`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p
    codex-rollout find_thread_path -- --nocapture`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p
    codex-rollout try_init_ -- --nocapture`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p
    codex-rollout`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo clippy -p
    codex-rollout --lib -- -D warnings`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p
    codex-thread-store
    read_thread_falls_back_when_sqlite_path_points_to_another_thread --
    --nocapture`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p
    codex-thread-store`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p codex-core
    shell_snapshot`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p codex-core
    --test all personality_migration`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p codex-core
    --test all rollout_list_find`
    - `RUST_MIN_STACK=8388608 CODEX_SKIP_VENDORED_BWRAP=1
    CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p codex-core
    --test all rollout_list_find::find_prefers_sqlite_path_by_id --
    --nocapture`
    - `RUST_MIN_STACK=8388608 CODEX_SKIP_VENDORED_BWRAP=1
    CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p codex-core
    --test all rollout_list_find -- --nocapture`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p codex-core
    interrupt_accounts_active_goal_before_pausing`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p
    codex-app-server get_auth_status -- --test-threads=1`
    - `CODEX_SKIP_VENDORED_BWRAP=1
    CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo test -p
    codex-app-server --lib`
    - `CODEX_SKIP_VENDORED_BWRAP=1
    CARGO_TARGET_DIR=/tmp/codex-target-state-db cargo check -p codex-rollout
    -p codex-app-server --tests`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db just fix -p codex-rollout
    -p codex-thread-store -p codex-core -p codex-app-server -p codex-tui -p
    codex-exec -p codex-cli`
    - `CODEX_SKIP_VENDORED_BWRAP=1
    CARGO_TARGET_DIR=/tmp/codex-target-state-db just fix -p codex-rollout -p
    codex-app-server`
    - `CARGO_TARGET_DIR=/tmp/codex-target-state-db just fix -p
    codex-rollout`
    - `CODEX_SKIP_VENDORED_BWRAP=1
    CARGO_TARGET_DIR=/tmp/codex-target-state-db just fix -p codex-core`
    - `just argument-comment-lint -p codex-core`
    - `just argument-comment-lint -p codex-rollout`
    
    Focused coverage added in `codex-rollout`:
    
    - `recorder::tests::state_db_init_backfills_before_returning` verifies
    the rollout metadata row exists before startup init returns.
    - `state_db::tests::try_init_waits_for_concurrent_startup_backfill`
    verifies startup waits for another worker to finish backfill instead of
    disabling the handle for the process.
    -
    `state_db::tests::try_init_times_out_waiting_for_stuck_startup_backfill`
    verifies startup does not hang indefinitely on a stuck backfill lease.
    -
    `tests::find_thread_path_accepts_existing_state_db_path_without_canonical_filename`
    verifies DB-backed lookup accepts valid existing rollout paths even when
    the filename does not include the thread UUID.
    -
    `tests::find_thread_path_falls_back_when_db_path_points_to_another_thread`
    verifies DB-backed lookup ignores a stale row whose existing path
    belongs to another thread and read-repairs the row after filesystem
    fallback.
    
    Focused coverage updated in `codex-core`:
    
    - `rollout_list_find::find_prefers_sqlite_path_by_id` now uses a
    DB-preferred rollout file with matching `session_meta.id`, so it still
    verifies that valid SQLite paths win without depending on stale/empty
    rollout contents.
    
    `cargo test -p codex-app-server thread_list_respects_search_term_filter
    -- --test-threads=1 --nocapture` was attempted locally but timed out
    waiting for the app-server test harness `initialize` response before
    reaching the changed thread-list code path.
    
    `bazel test //codex-rs/thread-store:thread-store-unit-tests
    --test_output=errors` was attempted locally after the thread-store fix,
    but this container failed before target analysis while fetching `v8+`
    through BuildBuddy/direct GitHub. The equivalent local crate coverage,
    including `cargo test -p codex-thread-store`, passes.
    
    A plain local `cargo check -p codex-rollout -p codex-app-server --tests`
    also requires system `libcap.pc` for `codex-linux-sandbox`; the
    follow-up app-server check above used `CODEX_SKIP_VENDORED_BWRAP=1` in
    this container.
  • feat(tui): add keymap debug inspector (#20794)
    ## Why
    
    We constantly get bug reports about keys not being recognized by Codex
    when the terminal is not handling the key press. Running `/keymap debug`
    or `/keymap` and going to the Debug tab, we can allow the user to either
    understand that the key being pressed is not being recognized or to
    check what it's being recognized as and report or reassign that key.
    
    | Menu | Inspector | Hint |
    |---|---|---|
    | <img width="1369" height="796" alt="CleanShot 2026-05-02 at 12 57 12"
    src="https://github.com/user-attachments/assets/512b6faa-344e-4aee-9c00-b4bdc633a662"
    /> | <img width="1261" height="754" alt="CleanShot 2026-05-02 at 12 56
    36"
    src="https://github.com/user-attachments/assets/a6ddae7d-e174-4ee4-893f-e6bec4fff4ab"
    /> | <img width="1369" height="796" alt="CleanShot 2026-05-02 at 12 57
    30"
    src="https://github.com/user-attachments/assets/db507784-f40a-4cff-ac23-a61d9703769b"
    /> |
    ## Summary
    - add a Debug tab to `/keymap` and support `/keymap debug` for direct
    access
    - show what key Codex receives, the config key representation, raw event
    details, and matching actions
    - add a progressive missing-key hint that escalates after a few seconds
    with no detected keypress
    
    ## Validation
    - `just fmt`
    - `cargo test -p codex-tui keymap_setup::tests::debug_view`
    - `cargo test -p codex-tui keymap_setup::tests`
    - `cargo test -p codex-tui slash_keymap`
    - `cargo test -p codex-tui` (unit tests passed; integration test
    `suite::model_availability_nux::resume_startup_does_not_consume_model_availability_nux_count`
    failed locally by itself with `codex resume` exiting 1 and terminal
    probe escape output)
    - `just fix -p codex-tui`
    - `just argument-comment-lint`
    - `cargo insta pending-snapshots`
    - `git diff --check`
  • Speed up /side parent restore replay (#20815)
    ## Why
    
    Returning from a `/side` conversation restores the parent thread by
    replaying its snapshot into the TUI. For very long parent threads,
    replaying every transcript row can take noticeable time even though most
    rows immediately scroll out of terminal history.
    
    ## What Changed
    
    - Buffer thread-switch replay for parent restores when terminal resize
    reflow is enabled.
    - Reuse the existing resize-reflow tail renderer so only the retained
    transcript tail is written back to scrollback when a row cap is
    configured.
  • /plugins: add marketplace upgrade flow (#20478)
    This PR adds marketplace upgrade to the `/plugins` menu so users can
    update configured marketplaces. It adds a `Ctrl+U` shortcut on eligible
    marketplace tabs, a loading state, and the app-server request flow
    needed to perform `marketplace/upgrade`. After a successful upgrade, the
    TUI refreshes plugin data, plugin mentions, and user config so updated
    marketplace contents show up across the menu and other plugin surfaces.
    It also preserves the current marketplace tab on no-op and failure paths
    and surfaces backend error details directly in the TUI.
    
    - Add a `Ctrl+U` upgrade option for user-configured marketplace tabs in
    `/plugins`
    - Show the upgrade footer hint only on upgradeable marketplace tabs
    - Show a loading state during `marketplace/upgrade`
    - Surface already-up-to-date and per-marketplace failure results from
    the backend
    - Refresh plugin data, plugin mentions, and user config after successful
    upgrades
    - Add tests and snapshot updates for the shortcut flow, loading state,
    and failure messaging
    
    Steps to test:
    1. Add a `/plugin` marketplace to Codex TUI.
    2. Open `/plugins`, move to that marketplace tab, and confirm the footer
    shows `Ctrl+U` to upgrade.
    3. Press `Ctrl+U` and confirm the popup switches into an upgrade loading
    state.
    4. When the request finishes, confirm you see the expected result:
    updated marketplace contents on success, an already-up-to-date message
    on no-op, or backend error details on failure. On no-op or failure,
    confirm the popup stays on the same marketplace tab.
  • Color TUI statusline from active theme (#19631)
    ## Why
    
    Users have shared that the TUI can feel too visually flat because themes
    mostly show up in code syntax highlighting. The configurable statusline
    is a natural place to make the active theme more visible, while still
    letting users keep the existing monotone statusline if they prefer it.
    
    ## What Changed
    
    - Added a statusline styling helper that builds the rendered statusline
    from `(StatusLineItem, text)` segments, preserving item identity while
    keeping the plain text output unchanged.
    - Derived foreground accent colors from the active syntax theme by
    looking up TextMate scopes through the existing syntax highlighter, with
    conservative ANSI fallbacks when a scope does not provide a foreground.
    - Tuned theme-derived colors to keep the accents visible without making
    the statusline feel overly bright.
    - Added `[tui].status_line_use_colors`, defaulting to `true`, plus a
    separated `/statusline` toggle so users can enable or disable
    theme-derived statusline colors from the setup UI.
    - Updated the live statusline and `/statusline` preview to use the same
    styled builder, while keeping terminal-title preview text plain.
    - Kept statusline separators and active-agent add-ons subdued while
    removing blanket dimming from the whole passive statusline.
    
    ## Verification
    
    - `cargo test -p codex-tui status_line`
    - `cargo test -p codex-tui theme_picker`
    - `cargo test -p codex-tui foreground_style_for_scopes`
    - `cargo test -p codex-tui`
    - `cargo test -p codex-config`
    - `cargo test -p codex-core status_line_use_colors`
    - `cargo insta pending-snapshots --manifest-path tui/Cargo.toml`
    
    ## Visual
    
    <img width="369" height="23" alt="Screenshot 2026-04-30 at 6 16 08 PM"
    src="https://github.com/user-attachments/assets/11d03efb-8e4f-4450-8f4d-00a9659ef4cd"
    />
    
    <img width="385" height="23" alt="Screenshot 2026-04-30 at 6 16 02 PM"
    src="https://github.com/user-attachments/assets/a3d89f36-bdc1-42e8-8e84-61350e3999e2"
    />
  • Add /hooks browser for lifecycle hooks (#19882)
    ## Why
    
    `hooks/list` and `hooks/config/write` give us read/write access to hooks
    and their state. This hooks up the TUI as a client so users can inspect
    and manage that state directly.
    
    ## What
    
    - add a two-page `/hooks` browser in the TUI: an event overview with
    installed/active counts, followed by a per-event handler page with
    toggle controls and detail rendering
    - thread managed-state metadata through hook discovery and `hooks/list`
    so the UI can label admin-managed hooks and suppress toggles for them
    - persist hook toggles through the existing config-write path and add
    snapshot coverage for the event list, handler list, managed-hook, and
    empty states
    
    ## Stack
    
    1. openai/codex#19705
    2. openai/codex#19778
    3. openai/codex#19840
    4. This PR - openai/codex#19882
    
    ## Reviewer Notes
    
    - Main UI logic is in
    `codex-rs/tui/src/bottom_pane/hooks_browser_view.rs`; most of the diff
    is the new view plus its snapshot coverage
    - Request / write plumbing for opening the browser and persisting
    toggles is in `codex-rs/tui/src/app/background_requests.rs` and
    `codex-rs/tui/src/chatwidget/hooks.rs`
    - Outside the TUI, the only behavioral change in this PR is threading
    `is_managed` through hook discovery and `hooks/list` so managed hooks
    render as non-toggleable
    - The `codex-rs/tui/src/status/snapshots/` churn is unrelated merge
    fallout from the stacked base branch's newer permission-label rendering
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Remove core protocol dependency [2/2] (#20325)
    ## Why
    
    With the local model layer and app-server routing in place from PR1,
    this PR moves the active TUI runtime onto app-server notifications. The
    affected pieces share the same event flow, so the command surface,
    session state, bottom-pane prompts, chat rendering, history/status
    views, and tests move together to keep the stacked branch buildable.
    
    This PR also removes the obsolete compatibility surface that is no
    longer used after the migration. The proposed protocol-boundary verifier
    layer was dropped from the stack; enforcing that final boundary will be
    simpler once `codex-tui` no longer needs any `codex_protocol`
    references.
    
    This PR is part 2 of a 2-PR stack:
    
    1. Add TUI-owned replacement models and extract app-server event
    routing.
    2. Move the active TUI flow to app-server notifications and delete
    obsolete adapter code.
    
    ## What changed
    
    - Rewired app command and session handling to use app-server request and
    notification shapes.
    - Moved approval overlays, request-user-input flows, MCP elicitation,
    realtime events, and review commands onto the app-server-facing model
    surface.
    - Updated chat rendering, history cells, status views, multi-agent UI,
    replay state, and TUI tests to use app-server notifications plus the
    local models introduced in PR1.
    - Deleted `codex-rs/tui/src/app/app_server_adapter.rs` and the
    superseded `chatwidget/tests/background_events.rs` fixture path.
    
    ## Verification
    
    - `cargo check -p codex-tui --tests`
    - Top of stack: `cargo test -p codex-tui`
  • /plugins: remove marketplace (#19843)
    This PR adds marketplace removal to the /plugins menu, giving users a
    way to remove user-configured plugin marketplaces. It adds a `Ctrl+R`
    shortcut to remove selected marketplace tabs, a confirmation prompt,
    loading and error states, and the app-server request flow needed to
    perform marketplace/remove. After a successful removal, the TUI
    refreshes config, plugin mentions, user config, and plugin data so the
    removed marketplace disappears from the menu and other surfaces in the
    TUI.
    
    - Add `Ctrl+R` removal option for user-configured marketplace tabs
    - Show marketplace removal confirmation, loading, and error states
    - Route `marketplace/remove` through the TUI background request flow
    - Refresh config, plugin mentions, and plugin data after successful
    removal
    - Adds reusable per-tab footer hints so removal guidance only appears on
    applicable tabs
    - Add test coverage for `Ctrl+R` behavior while plugin search is active
    
    Steps to test:
    - Add a marketplace using the TUI /plugins menu
    - Use Ctrl+R to remove the marketplace
    - Accept the confirmation prompt
    - Confirm the marketplace is removed when the process completes.
  • TUI: Remove core protocol dependency [3/7] (#20174)
    ## Why
    
    This is part 3 of a 7-PR stack to remove direct
    `codex_protocol::protocol` usage from `codex-tui` while keeping each
    layer reviewable and shippable.
    
    With `AppCommand` now explicit, the internal app event bus can carry TUI
    commands directly instead of bouncing through core `Op` values.
    
    ## What changed
    
    - Changed `AppEvent::CodexOp` and `AppEvent::SubmitThreadOp` to carry
    `AppCommand`.
    - Updated app-event senders and direct emitters to submit `AppCommand`
    values.
    - Adjusted tests to match `AppCommand` or convert back through
    `into_core()` where they intentionally assert legacy payload equality.
    
    ## Verification
    
    - `cargo test -p codex-tui --no-run`
  • /plugins: add marketplace install flow (#18704)
    This PR adds a new feature to the `/plugins` menu that gives users the
    ability to add new plugin marketplaces. It introduces an Add Marketplace
    tab to the right of installed marketplaces, a source prompt, loading and
    error states, and the app-server request flow needed to perform the
    install. After a successful `marketplace/add`, the popup refreshes back
    into the newly added marketplace tab so the new plugins are immediately
    visible.
    
    - Add an Add Marketplace tab to the `/plugins` menu
    - Prompt for marketplace source input from git repo, URL, or local path
    - Show loading and error states during `marketplace/add`
    - Refresh plugin data after success and switch into the newly added
    marketplace tab
    - Add tests and snapshot updates
  • tui: use permission profiles for sandbox state (#20008)
    ## Summary
    - Move TUI permission state from legacy `SandboxPolicy` values to
    canonical `PermissionProfile` values across presets, app events, chat
    widget state, app commands, thread routing, and cached thread session
    state.
    - Keep app-server compatibility boundaries explicit: embedded sessions
    send `permissionProfile`, while remote sessions send only a legacy
    `sandbox` projection and fall back to read-only when a custom profile
    cannot be projected.
    - Update status/add-dir UI summaries and snapshots to render the active
    permission profile, including workspace profiles selected by the new
    built-in defaults.
    
    ## Verification
    - `rg '\bSandboxPolicy\b' codex-rs/tui -n` returns no matches.
    - `cargo test -p codex-tui`
    - `cargo check -p codex-tui --tests`
    - `cargo test -p codex-tui additional_dirs`
    - `just fmt`
    - `just fix -p codex-tui`
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/20008).
    * #20041
    * #20040
    * #20037
    * #20035
    * #20034
    * #20033
    * #20032
    * #20030
    * #20028
    * #20027
    * #20026
    * #20024
    * #20021
    * #20018
    * #20016
    * #20015
    * #20013
    * #20011
    * #20010
    * __->__ #20008
  • feat(tui): add configurable keymap support (#18593)
    ## Why
    
    The TUI currently handles keyboard shortcuts as hard-coded event matches
    spread across app, composer, pager, list, approval, and navigation code.
    That makes shortcuts hard to customize, makes displayed hints easy to
    drift from actual behavior, and makes future keymap work riskier because
    there is no central action inventory.
    
    This PR adds the foundation for configurable, action-based keymaps
    without adding the interactive remapping UI yet. Onboarding
    intentionally stays on fixed startup shortcuts because users cannot
    reasonably configure keymaps before completing onboarding.
    
    This is PR1 in the keymap stack:
    
    - PR1: #18593: configurable keymap foundation
    - PR2: #18594: `/keymap` picker and guided remapping UI
    - PR3: #18595: Vim composer mode and the remap option
    
    ## Design Notes
    
    The new model resolves named actions into concrete runtime bindings once
    from config, then passes those bindings to the UI surfaces that handle
    input or render shortcut hints.
    
    The main concepts are:
    
    - **Context**: a scope where an action is active, such as `global`,
    `chat`, `composer`, `editor`, `pager`, `list`, or `approval`.
    - **Action**: a named operation inside a context, such as
    `global.open_transcript`, `composer.submit`, or `pager.close`.
    - **Binding**: one or more single-key shortcuts assigned to an action,
    written as config strings such as `ctrl-t`, `alt-backspace`, or
    `page-down`. Multi-step sequences such as `ctrl-x ctrl-s`, `g g`, or
    leader-key flows are not part of this PR.
    - **Resolution order**: context-specific config wins first, supported
    global fallbacks come next, and built-in defaults fill in anything
    unset.
    - **Explicit unbinding**: an empty array removes an action binding in
    that scope and does not fall through to a fallback binding.
    - **Conflict validation**: a resolved keymap rejects duplicate active
    bindings inside the same scope so one keypress cannot dispatch two
    actions.
    
    ## What Changed
    
    - Added `TuiKeymap` config support under `[tui.keymap]`, including typed
    contexts/actions, key alias normalization, generated schema coverage,
    and user-facing config errors.
    - Added `RuntimeKeymap` resolution in `codex-rs/tui/src/keymap.rs`,
    including fallback precedence, built-in defaults, explicit unbinding,
    and per-context conflict validation.
    - Rewired existing TUI handlers to consume resolved keymap actions
    instead of directly matching hard-coded keys in each component.
    - Updated key hint rendering and footer/pager/list surfaces so displayed
    shortcuts follow the resolved keymap.
    - Kept onboarding shortcuts fixed in
    `codex-rs/tui/src/onboarding/keys.rs` instead of exposing them through
    `[tui.keymap]`.
    
    ## Validation
    
    The branch includes focused coverage for config parsing, key
    normalization, runtime fallback resolution, explicit unbinding,
    duplicate-key conflict validation, default keymap consistency,
    onboarding startup key behavior, and UI hint snapshots affected by
    resolved key bindings.
  • Add /auto-review-denials retry approval flow (#19058)
    ## Why
    
    Auto-review can deny an action that the user later decides they want to
    retry. Today there is no TUI surface for selecting a recent denial and
    sending explicit approval context back into the session, so users have
    to restate intent manually and the retry can be reviewed without the
    original denied action context.
    
    This adds a narrow TUI-driven path for approving a recent denied action
    while still keeping the retry inside the normal auto-review flow.
    
    ## What Changed
    
    - Added `/auto-review-denials` to open a picker of recent denied
    auto-review actions.
    - Added a small in-memory TUI store for the 10 most recent denied
    auto-review events.
    - Selecting a denial sends the structured denied event back through the
    existing core/app-server op path.
    - Core now injects a developer message containing the approved action
    JSON rather than the full assessment event.
    - Auto-review transcript collection now preserves this specific approval
    developer message so follow-up review sessions can see the user approval
    context.
    - Added TUI snapshot/unit coverage for the picker and approval dispatch
    path.
    - Added core coverage for retaining the approval developer message in
    the auto-review transcript.
    
    ## Verification
    
    - `cargo test -p codex-core
    collect_guardian_transcript_entries_keeps_manual_approval_developer_message`
    - `cargo test -p codex-tui auto_review_denials`
    - `cargo test -p codex-tui
    approving_recent_denial_emits_structured_core_op_once`
    
    ## Notes
    
    This intentionally keeps retries going through auto-review. The approval
    signal is context for the exact previously denied action, not a blanket
    bypass for similar future actions.
  • permissions: centralize legacy sandbox projection (#19734)
    ## Why
    
    The remaining migration work still needs `SandboxPolicy` at a few
    compatibility boundaries, but those projections should come from one
    canonical path. Keeping ad hoc legacy projections scattered through
    app-server, CLI, and config code makes it easy for behavior to drift as
    `PermissionProfile` gains fidelity that the legacy enum cannot
    represent.
    
    ## What Changed
    
    - Adds `Permissions::legacy_sandbox_policy(cwd)` and
    `Config::legacy_sandbox_policy()` as the compatibility projection from
    the canonical `PermissionProfile`.
    - Adds `Permissions::can_set_legacy_sandbox_policy()` so legacy inputs
    are checked after they are converted into profile semantics.
    - Updates app-server command handling, Windows sandbox setup, session
    configuration, and sandbox summaries to use the centralized projection
    helper.
    - Leaves `SandboxPolicy` in place only for boundary inputs/outputs that
    still speak the legacy abstraction.
    
    ## Verification
    
    - `cargo check -p codex-config -p codex-core -p codex-sandboxing -p
    codex-app-server -p codex-cli -p codex-tui`
    - `cargo test -p codex-tui
    permissions_selection_history_snapshot_full_access_to_default --
    --nocapture`
    - `cargo test -p codex-tui
    permissions_selection_sends_approvals_reviewer_in_override_turn_context
    -- --nocapture`
    - `bazel test //codex-rs/tui:tui-unit-tests-bin
    --test_arg=permissions_selection_history_snapshot_full_access_to_default
    --test_output=errors`
    - `bazel test //codex-rs/tui:tui-unit-tests-bin
    --test_arg=permissions_selection_sends_approvals_reviewer_in_override_turn_context
    --test_output=errors`
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19734).
    * #19737
    * #19736
    * #19735
    * __->__ #19734
  • fix(tui): reflow scrollback on terminal resize (#18575)
    Fixes multiple scrollback and terminal resize issues: #5538, #5576,
    #8352, #12223, #16165, and #15380.
    
    ## Why
    
    Codex writes finalized transcript output into terminal scrollback after
    wrapping it for the current viewport width. A later terminal resize
    could leave that scrollback shaped for the old width, so wider windows
    kept narrow output and narrower windows could show stale wrapping
    artifacts until enough new output replaced the visible area.
    
    This is also the foundation PR for responsive markdown tables. Table
    rendering needs finalized transcript content to be width-sensitive after
    insertion, not only while content is first streaming. Markdown table
    rendering itself stays in #18576.
    
    ## Stack
    
    - PR1: resize backlog reflow and interrupt cleanup
    - #18576: markdown table support
    
    ## What Changed
    
    - Rebuild source-backed transcript history when the terminal width
    changes. `terminal_resize_reflow` is introduced through the experimental
    feature system, but is enabled by default for this rollout so we can
    validate behavior across real terminals.
    - Preserve assistant and plan stream source so finalized streaming
    output can participate in resize reflow after consolidation.
    - Debounce resize work, but force a final source-backed reflow when a
    resize happened during active or unconsolidated streaming output.
    - Clear stale pending history lines on resize so old-width wrapped
    output is not emitted just before rebuilt scrollback.
    - Bound replay work with `[tui.terminal_resize_reflow].max_rows`:
    omitted uses terminal-specific defaults, `0` keeps all rendered rows,
    and a positive value sets an explicit cap. The cap applies both while
    initially replaying a resumed transcript into scrollback and when
    rebuilding scrollback after terminal resize.
    - Consolidate interrupted assistant streams before cleanup, then clear
    pending stream output and active-tail state consistently.
    - Move resize reflow and thread event buffering helpers out of `app.rs`
    into dedicated TUI modules.
    - Add focused coverage for resize reflow, feature-gated behavior,
    streaming source preservation, interrupted output cleanup,
    unicode-neutral text, terminal-specific row caps, and composer/layout
    stability.
    
    ## Runtime Bounds
    
    Resize reflow keeps only the most recent rendered rows when a row cap is
    active. The default is `auto`, which maps to the detected terminal's
    default scrollback size where Codex can identify it: VS Code `1000`,
    Windows Terminal `9001`, WezTerm `3500`, and Alacritty `10000`.
    Terminals without a dedicated mapping use the conservative fallback of
    `1000` rows. Users can override this with `[tui.terminal_resize_reflow]
    max_rows = N`, or set `max_rows = 0` to disable row limiting.
    
    ## Validation
    
    - `just fmt`
    - `git diff --check`
    - `cargo test --manifest-path codex-rs/Cargo.toml -p codex-tui reflow`
    - `cargo test --manifest-path codex-rs/Cargo.toml -p codex-tui
    transcript_reflow`
    - `just fix -p codex-tui`
    - PR CI in progress on the squashed branch
  • Add goal TUI UX (5 / 5) (#18077)
    Adds the TUI user experience for goals on top of the core runtime from
    PR 4.
    
    ## Why
    
    Users need a direct TUI control surface for long-running goals. The UI
    should make the current goal visible, support common goal actions
    without waiting for a model turn, and avoid confusing end-of-turn
    notifications while an active goal is immediately continuing.
    
    ## What changed
    
    - Added `/goal` summary rendering for the current goal, including
    active, paused, budget-limited, and complete states.
    - Added `/goal <objective>` creation/replacement through the app-server
    goal API rather than a model prompt.
    - Added `/goal clear`, `/goal pause`, and `/goal unpause` command
    variants.
    - Added a confirmation menu when the user enters a new goal while
    another goal already exists.
    - Updated `/goal` help and summary tip text so it reflects the supported
    command variants without advertising slash-command token budgets.
    - Added footer/statusline goal indicators, including elapsed time and
    token budget display when a budget exists from API/tool-created goals.
    - Consumes goal updated/cleared notifications so the TUI stays in sync
    with external app-server changes.
    - Suppresses end-of-turn desktop notifications only when a goal is still
    active and follow-up work is expected.
    - Preserves slash-command history behavior and avoids leaking queued
    `/goal` state into unrelated submissions.
    
    ## Verification
    
    - Added TUI unit and snapshot coverage for goal command availability,
    summary rendering, control commands, replacement menu behavior,
    status/footer display, notification handling, and command history.
  • Fix /review interrupt and TUI exit wedges (#18921)
    Addresses #11267
    
    ## Summary
    `/review` can be interrupted while it is still spawning the review
    sub-agent. That spawn path lives in `codex-core` and did not observe the
    task cancellation token until after `Codex::spawn` returned, so an
    interrupted review could keep building a child session and leave the TUI
    in a wedged state.
    
    The TUI exit path also waited indefinitely for app-server
    `thread/unsubscribe`, which made Ctrl+C look broken if the app-server
    was already stuck. This makes interactive delegate startup
    cancellation-aware and bounds the TUI shutdown-first unsubscribe wait
    with a short UI escape-hatch timeout.
    
    ## Testing
    I reproed the hang using the steps in the bug report. Confirmed hang no
    longer exists after fix.
  • TUI: preserve permission state after side conversations (#18924)
    Addresses #18854
    
    ## Why
    
    The `/permissions` selector updates the active TUI session state, but
    the cached session snapshot used when replaying a thread could still
    contain the old approval or sandbox settings. After opening and leaving
    `/side`, the main thread replay could restore those stale settings into
    the `ChatWidget`, so the UI and the next submitted turn could fall back
    to the old permission mode.
    
    ## What
    
    - Sync the active thread's cached `ThreadSessionState` whenever approval
    policy, sandbox policy, or approval reviewer changes.
    
    ## Verification
    
    Confirmed bug prior to fix and correct behavior after fix.
  • Default Fast service tier for eligible ChatGPT plans (#19053)
    ## Why
    
    Enterprise and business-like ChatGPT plans should get Codex's Fast
    service tier by default when the user or caller has not made an explicit
    service-tier choice. At the same time, callers need a durable way to
    choose standard routing without adding a new persisted `standard`
    service tier value. This keeps existing config compatibility while
    letting core own the managed default policy.
    
    ## What changed
    
    - Resolve the effective service tier in core at session creation:
    explicit `fast` or `flex` wins, explicit null/clear or
    `[notice].fast_default_opt_out = true` resolves to standard routing, and
    otherwise eligible ChatGPT plans resolve to Fast when FastMode is
    enabled.
    - Add `[notice].fast_default_opt_out` as the persisted opt-out marker
    for managed Fast defaults.
    - Treat app-server/TUI `service_tier: null` as an explicit
    standard/clear choice by preserving that intent through config loading.
    - Update TUI rendering to use core's effective service tier for startup
    and status surfaces while still keeping `config.service_tier` as the
    explicit configured choice.
    - Update `/fast off` to clear `service_tier`, persist the opt-out
    marker, and send explicit standard for subsequent turns.
    
    ## Verification
    
    - Added unit coverage for config override/notice handling, service-tier
    resolution, runtime null clearing, and `/fast off` turn propagation.
    - `cargo build -p codex-cli`
    
    Full test suite was not run locally per author request.
  • Refactor TUI app module into submodules (#18753)
    ## Why
    
    The TUI app module had grown past the 512K source-file cap enforced by
    CI/CD. This keeps the app entry point below that limit while preserving
    the existing runtime behavior and test surface.
    
    ## What changed
    
    - Kept the top-level `App` state and run-loop wiring in
    `tui/src/app.rs`.
    - Split app responsibilities into focused private submodules under
    `tui/src/app/`, covering event dispatch, thread routing, session
    lifecycle, config persistence, background requests, startup prompts,
    input, history UI, platform actions, and thread event buffering.
    - Moved the existing app-level tests into `tui/src/app/tests.rs` and
    reused the existing snapshot location rather than adding new tests or
    snapshots.
    - Added module header comments for `app.rs` and the new submodules.
    
    ## Follow-up
    
    A future cleanup can move narrow unit tests from `tui/src/app/tests.rs`
    into the specific app submodules they exercise. This PR keeps the
    existing app-level tests together so the refactor stays focused on the
    source-file split.
    
    ## Verification
    
    - `cargo test -p codex-tui --lib
    app::tests::agent_picker_item_name_snapshot`
    - `cargo test -p codex-tui --lib app::tests::clear_ui`
    - `cargo test -p codex-tui --lib
    app::tests::ctrl_l_clear_ui_after_long_transcript_reuses_clear_header_snapshot`
    - `just fix -p codex-tui`
    
    Full `cargo test -p codex-tui` still fails on model-catalog drift
    unrelated to this refactor, including stale
    `gpt-5.3-codex`/`gpt-5.1-codex` snapshot and migration expectations now
    resolving to `gpt-5.4`.