Commit Graph

1527 Commits

  • feat: reset memories button (#17937)
    <img width="720" height="175" alt="Screenshot 2026-04-15 at 14 35 02"
    src="https://github.com/user-attachments/assets/041d73ff-8c16-42a9-8e92-c245805084f0"
    />
  • feat: memories menu (#17632)
    Add menu that:
    1. If memories feature is not enabled, propose to enable it
    2. Let you choose if you want to generate memories and to use memories
  • Spread AbsolutePathBuf (#17792)
    Mechanical change to promote absolute paths through code.
  • Fix remote skill popup loading (#17702)
    ## Summary
    
    Fix the TUI `$` skill popup so personal skills appear reliably when
    Codex is connected to a remote app-server.
    
    ## What changed
    
    - load skills on TUI startup with an explicit forced refresh
    - refresh skills using the actual current cwd instead of an empty `cwds`
    list
    - resync an already-open `$` popup when skill mentions are updated
    - add a regression test for refreshing an open mention popup
    
    ## Root cause
    
    The TUI was sometimes sending `list_skills` with `cwds: []` after
    `SessionConfigured`.
    
    For the launchd app-server flow, the server resolved that empty cwd list
    to its own process cwd, which was `/`. The response therefore came back
    tagged with `cwd: "/"`, and the TUI later filtered skills by exact cwd
    match against the actual project cwd such as `/Users/starr/code/dream`.
    That dropped all personal skills from the mention list, so `$` only
    showed plugins/apps.
    
    ## Verification
    
    Built successfully with remote cache disabled:
    
    ```bash
    cd /Users/starr/code/codex-worktrees/starr-skill-popup-20260413130509
    bazel --output_base=/tmp/codex-bazel-verify-starr-skill-popup build //codex-rs/cli:codex --noremote_accept_cached --noremote_upload_local_results --disk_cache=
    ```
    
    Also verified interactively in a PTY against the live app-server at
    `ws://127.0.0.1:4511`:
    - launched the built TUI
    - typed `$`
    - confirmed personal skills appeared in the popup, including entries
    such as `Applied Devbox`, `CI Debug`, `Channel Summarization`, `Codex PR
    Review`, and `Daily Digest`
    
    ## Files changed
    
    - `codex-rs/tui/src/app.rs`
    - `codex-rs/tui/src/chatwidget.rs`
    - `codex-rs/tui/src/bottom_pane/chat_composer.rs`
    
    Co-authored-by: Codex <noreply@openai.com>
  • fix: Revert danger-full-access denylist-only mode (#17732)
    ## Summary
    
    - Reverts openai/codex#16946 and removes the danger-full-access
    denylist-only network mode.
    - Removes the corresponding config requirements, app-server
    protocol/schema, config API, TUI debug output, and network proxy
    behavior.
    - Drops stale tests that depended on the reverted mode while preserving
    newer managed allowlist-only coverage.
    
    ## Verification
    
    - `just write-app-server-schema`
    - `just fmt`
    - `cargo test -p codex-config network_requirements`
    - `cargo test -p codex-core network_proxy_spec`
    - `cargo test -p codex-core
    managed_network_proxy_decider_survives_full_access_start`
    - `cargo test -p codex-app-server map_requirements_toml_to_api`
    - `cargo test -p codex-tui debug_config_output`
    - `cargo test -p codex-app-server-protocol`
    - `just fix -p codex-config -p codex-core -p codex-app-server-protocol
    -p codex-app-server -p codex-tui`
    - `git diff --cached --check`
    
    Not run: full workspace `cargo test` (repo instructions ask for
    confirmation before that broader run).
  • Add realtime output modality and transcript events (#17701)
    - Add outputModality to thread/realtime/start and wire text/audio output
    selection through app-server, core, API, and TUI.\n- Rename the realtime
    transcript delta notification and add a separate transcript done
    notification that forwards final text from item done without correlating
    it with deltas.
  • Refactor plugin loading to async (#17747)
    Simplifies skills migration.
  • [codex] Remove unused Rust helpers (#17146)
    ## Summary
    
    Removes high-confidence unused Rust helper functions and exports across
    `codex-tui`, `codex-shell-command`, and utility crates.
    
    The cleanup includes dead TUI helper methods, unused
    path/string/elapsed/fuzzy-match utilities, an unused Windows PowerShell
    lookup helper, and the unused terminal palette version counter. This
    keeps the remaining public surface smaller without changing behavior.
    
    ## Validation
    
    - `just fmt`
    - `cargo test -p codex-tui -p codex-shell-command -p codex-utils-elapsed
    -p codex-utils-fuzzy-match -p codex-utils-string -p codex-utils-path`
    - `just fix -p codex-tui -p codex-shell-command -p codex-utils-elapsed
    -p codex-utils-fuzzy-match -p codex-utils-string -p codex-utils-path`
    - `git diff --check`
  • guardian timeout fix pr 3 - ux touch for timeouts (#17557)
    This PR teaches the TUI to render guardian review timeouts as explicit
    terminal history entries instead of dropping them from the live
    timeline.
    It adds timeout-specific history cells for command, patch, MCP tool, and
    network approval reviews.
    It also adds snapshot tests covering both the direct guardian event path
    and the app-server notification path.
  • Fix tui compilation (#17691)
    The recent release broke, codex suggested this as the fix
    
    Source failure:
    https://github.com/openai/codex/actions/runs/24362949066/job/71147202092
    
    Probably from
    https://github.com/openai/codex/commit/ac82443d073f7f9a2248bad51bae2fa424ef4946
    
    For why it got in:
    ```
    The relevant setup:
    
    .github/workflows/rust-ci.yml (line 1) runs on PRs, but for codex-rs it only does:
    
    cargo fmt --check
    cargo shear
    argument-comment lint via Bazel
    no cargo check, no cargo clippy over the workspace, no cargo test over codex-tui
    .github/workflows/rust-ci-full.yml (line 1) runs on pushes to main and branches matching **full-ci**. That one does compile TUI because:
    
    codex-rs/Cargo.toml includes "tui" as a workspace member
    lint_build runs cargo clippy --target ... --tests --profile ...
    the matrix includes both dev and release profiles
    tests runs cargo nextest run ..., but only dev-profile tests
    Release CI also compiles it indirectly. .github/workflows/rust-release.yml (line 235) builds --bin codex, and cli/Cargo.toml (line 46) depends on codex-tui.
    ```
    
    Codex tested locally with `cargo check -p codex-tui --release` and was
    able to repro, and verified that this fixed it
  • Fix TUI compaction item replay (#17657)
    Problem: PR #17601 updated context-compaction replay to call a new
    ChatWidget handler, but the handler was never implemented, breaking
    codex-tui compilation on main.
    
    Solution: Render context-compaction replay through the existing
    info-message path, preserving the intended `Context compacted` UI marker
    without adding a one-off handler.
  • Suppress duplicate compaction and terminal wait events (#17601)
    Addresses #17514
    
    Problem: PR #16966 made the TUI render the deprecated context-compaction
    notification, while v2 could also receive legacy unified-exec
    interaction items alongside terminal-interaction notifications, causing
    duplicate "Context compacted" and "Waited for background terminal"
    messages.
    
    Solution: Suppress deprecated context-compaction notifications and
    legacy unified-exec interaction command items from the app-server v2
    projection, and render canonical context-compaction items through the
    existing TUI info-event path.
  • Wrap status reset timestamps in narrow layouts (#17481)
    Addresses #17453
    
    Problem: /status rate-limit reset timestamps can be truncated in narrow
    layouts, leaving users with partial times or dates.
    
    Solution: Let narrow rate-limit rows drop the fixed progress bar to
    preserve the percent summary, and wrap reset timestamps onto
    continuation lines instead of truncating them.
  • Emit plan-mode prompt notifications for questionnaires (#17417)
    Addresses #17252
    
    Problem: Plan-mode clarification questionnaires used the generic
    user-input notification type, so configs listening for plan-mode-prompt
    did not fire when request_user_input waited for an answer.
    
    Solution: Map request_user_input prompts to the plan-mode-prompt
    notification and remove the obsolete user-input TUI notification
    variant.
  • Run exec-server fs operations through sandbox helper (#17294)
    ## Summary
    - run exec-server filesystem RPCs requiring sandboxing through a
    `codex-fs` arg0 helper over stdin/stdout
    - keep direct local filesystem execution for `DangerFullAccess` and
    external sandbox policies
    - remove the standalone exec-server binary path in favor of top-level
    arg0 dispatch/runtime paths
    - add sandbox escape regression coverage for local and remote filesystem
    paths
    
    ## Validation
    - `just fmt`
    - `git diff --check`
    - remote devbox: `cd codex-rs && bazel test --bes_backend=
    --bes_results_url= //codex-rs/exec-server:all` (6/6 passed)
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Expose instruction sources (AGENTS.md) via app server (#17506)
    Addresses #17498
    
    Problem: The TUI derived /status instruction source paths from the local
    client environment, which could show stale <none> output or incorrect
    paths when connected to a remote app server.
    
    Solution: Add an app-server v2 instructionSources snapshot to thread
    start/resume/fork responses, default it to an empty list when older
    servers omit it, and render TUI /status from that server-provided
    session data.
    
    Additional context: The app-server field is intentionally named
    instructionSources rather than AGENTS.md-specific terminology because
    the loaded instruction sources can include global instructions, project
    AGENTS.md files, AGENTS.override.md, user-defined instruction files, and
    future dynamic sources.
  • Remove context status-line meter (#17420)
    Addresses #17313
    
    Problem: The visual context meter in the status line was confusing and
    continued to draw negative feedback, and context reporting should remain
    an explicit opt-in rather than part of the default footer.
    
    Solution: Remove the visual meter, restore opt-in context remaining/used
    percentage items that explicitly say "Context", keep existing
    context-usage configs working as a hidden alias, and update the setup
    text and snapshots.
  • feat(tui): add reverse history search to composer (#17550)
    ## Problem
    
    The TUI had shell-style Up/Down history recall, but `Ctrl+R` did not
    provide the reverse incremental search workflow users expect from
    shells. Users needed a way to search older prompts without immediately
    replacing the current draft, and the interaction needed to handle async
    persistent history, repeated navigation keys, duplicate prompt text,
    footer hints, and preview highlighting without making the main composer
    file even harder to review.
    
    
    https://github.com/user-attachments/assets/5165affd-4c9a-46e9-adbd-89088f5f7b6b
    
    <img width="1227" height="722" alt="image"
    src="https://github.com/user-attachments/assets/8bc83289-eeca-47c7-b0c3-8975101901af"
    />
    
    ## Mental model
    
    `Ctrl+R` opens a temporary search session owned by the composer. The
    footer line becomes the search input, the composer body previews the
    current match only after the query has text, and `Enter` accepts that
    preview as an editable draft while `Esc` restores the draft that existed
    before search started. The history layer provides a combined offset
    space over persistent and local history, but search navigation exposes
    unique prompt text rather than every physical history row.
    
    ## Non-goals
    
    This change does not rewrite stored history, change normal Up/Down
    browsing semantics, add fuzzy matching, or add persistent metadata for
    attachments in cross-session history. Search deduplication is
    deliberately scoped to the active Ctrl+R search session and uses exact
    prompt text, so case, whitespace, punctuation, and attachment-only
    differences are not normalized.
    
    ## Tradeoffs
    
    The implementation keeps search state in the existing composer and
    history state machines instead of adding a new cross-module controller.
    That keeps ownership local and testable, but it means the composer still
    coordinates visible search status, draft restoration, footer rendering,
    cursor placement, and match highlighting while `ChatComposerHistory`
    owns traversal, async fetch continuation, boundary clamping, and
    unique-result caching. Unique-result caching stores cloned
    `HistoryEntry` values so known matches can be revisited without cache
    lookups; this is simple and robust for interactive search sizes, but it
    is not a global history index.
    
    ## Architecture
    
    `ChatComposer` detects `Ctrl+R`, snapshots the current draft, switches
    the footer to `FooterMode::HistorySearch`, and routes search-mode keys
    before normal editing. Query edits call `ChatComposerHistory::search`
    with `restart = true`, which starts from the newest combined-history
    offset. Repeated `Ctrl+R` or Up searches older; Down searches newer
    through already discovered unique matches or continues the scan.
    Persistent history entries still arrive asynchronously through
    `on_entry_response`, where a pending search either accepts the response,
    skips a duplicate, or requests the next offset.
    
    The composer-facing pieces now live in
    `codex-rs/tui/src/bottom_pane/chat_composer/history_search.rs`, leaving
    `chat_composer.rs` responsible for routing and rendering integration
    instead of owning every search helper inline.
    `codex-rs/tui/src/bottom_pane/chat_composer_history.rs` remains the
    owner of stored history, combined offsets, async fetch state, boundary
    semantics, and duplicate suppression. Match highlighting is computed
    from the current composer text while search is active and disappears
    when the match is accepted.
    
    ## Observability
    
    There are no new logs or telemetry. The practical debug path is state
    inspection: `ChatComposer.history_search` tells whether the footer query
    is idle, searching, matched, or unmatched; `ChatComposerHistory.search`
    tracks selected raw offsets, pending persistent fetches, exhausted
    directions, and unique match cache state. If a user reports skipped or
    repeated results, first inspect the exact stored prompt text, the
    selected offset, whether an async persistent response is still pending,
    and whether a query edit restarted the search session.
    
    ## Tests
    
    The change is covered by focused `codex-tui` unit tests for opening
    search without previewing the latest entry, accepting and canceling
    search, no-match restoration, boundary clamping, footer hints,
    case-insensitive highlighting, local duplicate skipping, and persistent
    duplicate skipping through async responses. Snapshot coverage captures
    the footer-mode visual changes. Local verification used `just fmt`,
    `cargo test -p codex-tui history_search`, `cargo test -p codex-tui`, and
    `just fix -p codex-tui`.
  • Mirror user text into realtime (#17520)
    - Let typed user messages submit while realtime is active and mirror
    accepted text into the realtime text stream.
    - Add integration coverage and snapshot for outbound realtime text.
  • Restore codex-tui resume hint on exit (#17415)
    Addresses #17303
    
    Problem: The standalone codex-tui entrypoint only printed token usage on
    exit, so resumable sessions could omit the codex resume footer even when
    thread metadata was available.
    
    Solution: Format codex-tui exit output from AppExitInfo so it includes
    the same resume hint as the main CLI and reports fatal exits
    consistently.
  • Clear /ps after /stop (#17416)
    Addresses #17311
    
    Problem: `/stop` stops background terminals, but `/ps` can still show
    stale entries because the TUI process cache is cleared only after later
    exec end events arrive.
    
    Solution: Clear the TUI's tracked unified exec process list and footer
    immediately when `/stop` submits background terminal cleanup.
  • Support prolite plan type (#17419)
    Addresses #17353
    
    Problem: Codex rate-limit fetching failed when the backend returned the
    new `prolite` subscription plan type.
    
    Solution: Add `prolite` to the backend/account/auth plan mappings, keep
    unknown WHAM plan values decodable, and regenerate app-server plan
    schemas.
  • Handle closed TUI input stream as shutdown (#17430)
    Addresses #17276
    
    Problem: Closing the terminal while the TUI input stream is pending
    could leave the app outside the normal shutdown path, which is risky
    when an approval prompt is active.
    
    Solution: Treat a closed TUI input stream as ShutdownFirst so existing
    thread shutdown behavior cancels pending work and approvals before exit.
  • fix(tui): recall accepted slash commands locally (#17336)
    # TL;DR
    
    - Adds recognized slash commands to the TUI's local in-session recall
    history.
    - This is the MVP of the whole feature: it keeps slash-command recall
    local only: nothing is written to persistent history, app-server
    history, or core history storage.
    - Treats slash commands like submitted text once they parse as a known
    built-in command, regardless of whether command dispatch later succeeds.
    
    # Problem
    
    Slash commands are handled outside the normal message submission path,
    so they could clear the composer without becoming part of the local
    Up-arrow recall list. That made command-heavy workflows awkward: after
    running `/diff`, `/rename Better title`, `/plan investigate this`, or
    even a valid command that reports a usage error, users had to retype the
    command instead of recalling and editing it like a normal prompt.
    
    The goal of this PR is to make slash commands feel like submitted input
    inside the current TUI session while keeping the change deliberately
    local. This is not persistent history yet; it only affects the
    composer's in-memory recall behavior.
    
    # Mental model
    
    The composer owns draft state and local recall. When slash input parses
    as a recognized built-in command, the composer stages the submitted
    command text before returning `InputResult::Command` or
    `InputResult::CommandWithArgs`. `ChatWidget` then dispatches the command
    and records the staged entry once dispatch returns to the input-result
    path.
    
    Command-name recognition is the only validation before local recall. A
    valid slash command is recallable whether it succeeds, fails with a
    usage error, no-ops, is unavailable while a task is running, or is
    skipped by command-specific logic. An unrecognized slash command is
    different: it is restored as a draft, surfaces the existing
    unrecognized-command message, and is not added to recall.
    
    Bare commands recalled from typed text use the trimmed submitted draft.
    Commands selected from the popup record the canonical command text, such
    as `/diff`, rather than the partial filter text the user typed. Inline
    commands with arguments keep the original command invocation available
    locally even when their arguments are later prepared through the normal
    submission pipeline.
    
    # Non-goals
    
    Persisting slash commands across sessions is intentionally out of scope.
    This change does not modify app-server history, core history storage,
    protocol events, or message submission semantics.
    
    This does not change command availability, command side effects, popup
    filtering, command parsing, or the semantics of unsupported commands. It
    only changes whether recognized slash-command invocations are available
    through local Up-arrow recall after the user submits them.
    
    # Tradeoffs
    
    The main tradeoff is that recall is based on command recognition, not
    command outcome. This intentionally favors a simpler user model: if the
    TUI accepted the input as a slash command, the user can recall and edit
    that input just like plain text. That means valid-but-unsuccessful
    invocations such as usage errors are recallable, which is useful when
    the next action is usually to edit and retry.
    
    The previous accept/reject design required command dispatch to report a
    boolean outcome, which made the dispatcher API noisier and forced every
    branch to decide history behavior. This version keeps the dispatch APIs
    as side-effect-only methods and localizes history recording to the
    slash-command input path.
    
    Inline command handling still avoids double-recording by preparing
    inline arguments without using the normal message-submission history
    path. The staged slash-command entry remains the single local recall
    record for the command invocation.
    
    # Architecture
    
    `ChatComposer` stages a pending `HistoryEntry` when recognized
    slash-command input is promoted into an input result. The pending entry
    mirrors the existing local history payload shape so recall can restore
    text elements, local images, remote images, mention bindings, and
    pending paste state when those are present.
    
    `BottomPane` exposes a narrow method for recording that staged command
    entry because it owns the composer. `ChatWidget` records the staged
    entry after dispatching a recognized command from the input-result
    match. Valid commands rejected before they reach `ChatWidget`, such as
    commands unavailable while a task is running, are staged and recorded in
    the composer path that detects the rejection.
    
    Slash-command dispatch itself now lives in
    `chatwidget/slash_dispatch.rs` so the behavior is reviewable without
    adding more weight to `chatwidget.rs`. The extraction is
    behavior-preserving: the dispatch match arms stay intact, while the
    input flow in `chatwidget.rs` remains the single place that connects
    submitted slash-command input to dispatch.
    
    # Observability
    
    There is no new logging because this is a local UI recall behavior and
    the result is directly visible through Up-arrow recall. The practical
    debug path is to trace Enter through
    `ChatComposer::try_dispatch_bare_slash_command`,
    `ChatComposer::try_dispatch_slash_command_with_args`, or popup Enter/Tab
    handling, then confirm the recognized command is staged before dispatch
    and recorded exactly once afterward.
    
    If a valid command unexpectedly does not appear in recall, check whether
    the input path staged slash history before clearing the composer and
    whether it used the `ChatWidget` slash-dispatch wrapper. If an
    unrecognized command unexpectedly appears in recall, check the parser
    branch that should restore the draft instead of staging history.
    
    # Tests
    
    Composer-level tests cover staging and recording for a bare typed slash
    command, a popup-selected command, and an inline command with arguments.
    
    Chat-widget tests cover valid commands being recallable after normal
    dispatch, inline dispatch, usage errors, task-running unavailability,
    no-op stub dispatch, and command-specific skip behavior such as `/init`
    when an instructions file already exists. They also cover the negative
    case: unrecognized slash commands are not added to local recall.
  • Pass turn id with feedback uploads (#17314)
    ## Summary
    - Add an optional `tags` dictionary to feedback upload params.
    - Capture the active app-server turn id in the TUI and submit it as
    `tags.turn_id` with `/feedback` uploads.
    - Merge client-provided feedback tags into Sentry feedback tags while
    preserving reserved system fields like `thread_id`, `classification`,
    `cli_version`, `session_source`, and `reason`.
    
    ## Behavior / impact
    Existing feedback upload callers remain compatible because `tags` is
    optional and nullable. The wire shape is still a normal JSON object /
    TypeScript dictionary, so adding future feedback metadata will not
    require a new top-level protocol field each time. This change only adds
    feedback metadata for Codex CLI/TUI uploads; it does not affect existing
    pipelines, DAGs, exports, or downstream consumers unless they choose to
    read the new `turn_id` feedback tag.
    
    ## Tests
    - `cargo fmt -- --config imports_granularity=Item` passed; stable
    rustfmt warned that `imports_granularity` is nightly-only.
    - `cargo run -p codex-app-server-protocol --bin write_schema_fixtures`
    - `cargo test -p codex-feedback
    upload_tags_include_client_tags_and_preserve_reserved_fields`
    - `cargo test -p codex-app-server-protocol
    schema_fixtures_match_generated`
    - `cargo test -p codex-tui build_feedback_upload_params`
    - `cargo test -p codex-tui
    live_app_server_turn_started_sets_feedback_turn_id`
    - `cargo check -p codex-app-server --tests`
    - `git diff --check`
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Fix thread/list cwd filtering for Windows verbatim paths (#17414)
    Addresses #17302
    
    Problem: `thread/list` compared cwd filters with raw path equality, so
    `resume --last` could miss Windows sessions when the saved cwd used a
    verbatim path form and the current cwd did not.
    
    Solution: Normalize cwd comparisons through the existing path comparison
    utilities before falling back to direct equality, and add Windows
    regression coverage for verbatim paths. I made this a general utility
    function and replaced all of the duplicated instance of it across the
    code base.
  • TUI: enforce core boundary (#17399)
    Problem: The TUI still depended on `codex-core` directly in a number of
    places, and we had no enforcement from keeping this problem from getting
    worse.
    
    Solution: Route TUI core access through
    `codex-app-server-client::legacy_core`, add CI enforcement for that
    boundary, and re-export this legacy bridge inside the TUI as
    `crate::legacy_core` so the remaining call sites stay readable. There is
    no functional change in this PR — just changes to import targets.
    
    Over time, we can whittle away at the remaining symbols in this legacy
    namespace with the eventual goal of removing them all. In the meantime,
    this linter rule will prevent us from inadvertently importing new
    symbols from core.
  • representing guardian review timeouts in protocol types (#17381)
    ## Summary
    
    - Add `TimedOut` to Guardian/review carrier types:
      - `ReviewDecision::TimedOut`
      - `GuardianAssessmentStatus::TimedOut`
      - app-server v2 `GuardianApprovalReviewStatus::TimedOut`
    - Regenerate app-server JSON/TypeScript schemas for the new wire shape.
    - Wire the new status through core/app-server/TUI mappings with
    conservative fail-closed handling.
    - Keep `TimedOut` non-user-selectable in the approval UI.
    
    **Does not change runtime behavior yet; emitting `TimeOut` and
    parent-model timeout messaging will come in followup PRs**
  • fix(permissions): fix symlinked writable roots in sandbox permissions (#15981)
    ## Summary
    - preserve logical symlink paths during permission normalization and
    config cwd handling
    - bind real targets for symlinked readable/writable roots in bwrap and
    remap carveouts and unreadable roots there
    - add regressions for symlinked carveouts and nested symlink escape
    masking
    
    ## Root cause
    Permission normalization canonicalized symlinked writable roots and cwd
    to their real targets too early. That drifted policy checks away from
    the logical paths the sandboxed process can actually address, while
    bwrap still needed the real targets for mounts. The mismatch caused
    shell and apply_patch failures on symlinked writable roots.
    
    ## Impact
    Fixes #15781.
    
    Also fixes #17079:
    - #17079 is the protected symlinked carveout side: bwrap now binds the
    real symlinked writable-root target and remaps carveouts before masking.
    
    Related to #15157:
    - #15157 is the broader permission-check side of this path-identity
    problem. This PR addresses the shared logical-vs-canonical normalization
    issue, but the reported Darwin prompt behavior should be validated
    separately before auto-closing it.
    
    This should also fix #14672, #14694, #14715, and #15725:
    - #14672, #14694, and #14715 are the same Linux
    symlinked-writable-root/bwrap family as #15781.
    - #15725 is the protected symlinked workspace path variant; the PR
    preserves the protected logical path in policy space while bwrap applies
    read-only or unreadable treatment to the resolved target so
    file-vs-directory bind mismatches do not abort sandbox setup.
    
    ## Notes
    - Added Linux-only regressions for symlinked writable ancestors and
    protected symlinked directory targets, including nested symlink escape
    masking without rebinding the escape target writable.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Revert "Option to Notify Workspace Owner When Usage Limit is Reached" (#17391)
    Reverts openai/codex#16969
    
    #sev3-2026-04-10-accountscheckversion-500s-for-openai-workspace-7300
  • fix(guardian, app-server): introduce guardian review ids (#17298)
    ## Description
    
    This PR introduces `review_id` as the stable identifier for guardian
    reviews and exposes it in app-server `item/autoApprovalReview/started`
    and `item/autoApprovalReview/completed` events.
    
    Internally, guardian rejection state is now keyed by `review_id` instead
    of the reviewed tool item ID. `target_item_id` is still included when a
    review maps to a concrete thread item, but it is no longer overloaded as
    the review lifecycle identifier.
    
    ## Motivation
    
    We'd like to give users the ability to preempt a guardian review while
    it's running (approve or decline).
    
    However, we can't implement the API that allows the user to override a
    running guardian review because we didn't have a unique `review_id` per
    guardian review. Using `target_item_id` is not correct since:
    - with execve reviews, there can be multiple execve calls (and therefore
    guardian reviews) per shell command
    - with network policy reviews, there is no target item ID
    
    The PR that actually implements user overrides will use `review_id` as
    the stable identifier.
  • Support clear SessionStart source (#17073)
    ## Motivation
    
    The `SessionStart` hook already receives `startup` and `resume` sources,
    but sessions created from `/clear` previously looked like normal startup
    sessions. This makes it impossible for hook authors to distinguish
    between these with the matcher.
    
    ## Summary
    
    - Add `InitialHistory::Cleared` so `/clear`-created sessions can be
    distinguished from ordinary startup sessions.
    - Add `SessionStartSource::Clear` and wire it through core, app-server
    thread start params, and TUI clear-session flow.
    - Update app-server protocol schemas, generated TypeScript, docs, and
    related tests.
    
    
    https://github.com/user-attachments/assets/9cae3cb4-41c7-4d06-b34f-966252442e5c
  • [codex] Improve hook status rendering (#17266)
    # Motivation
    
    Make hook display less noisy and more useful by keeping transient hook
    activity out of permanent history unless there is useful output,
    preserving visibility for meaningful hook work, and making completed
    hook severity easier to scan.
    
    Also addresses some of the concerns in
    https://github.com/openai/codex/issues/15497
    
    # Changes
    
    ## Demo
    
    
    https://github.com/user-attachments/assets/9d8cebd4-a502-4c95-819c-c806c0731288
    
    Reverse spec for the behavior changes in this branch:
    
    ## Hook Lifecycle Rendering
    - Hook start events no longer write permanent history rows like `Running
    PreToolUse hook`.
    - Running hooks now render in a dedicated live hook area above the
    composer. It's similar to the active cell we use for tool calls but its
    a separate lane.
    - Running hook rows use the existing animation setting.
    
    ## Hook Reveal Timing
    - We wait 300ms before showing running hook rows and linger for up to
    600ms once visible.
    - This is so fast hooks don't flash a transient `Running hook` row
    before user can read it every time.
    - If a fast hook completes with meaningful output, only the completed
    hook result is written to history.
    - If a fast hook completes successfully with no output, it leaves no
    visible trace.
    
    ## Completed Hook Output
    - Completed hooks with output are sticky, for example `• SessionStart
    hook (completed)`.
    - Hook output entries are rendered under that row with stable prefixes:
    `warning:`, `stop:`, `feedback:`, `hook context:`, and `error:`.
    - Blocked hooks show feedback entries, for example `• PreToolUse hook
    (blocked)` followed by `feedback: ...`.
    - Failed hooks show error entries, for example `• PostToolUse hook
    (failed)` followed by `error: ...`.
    - Stopped hooks show stop entries and remain visually treated as
    non-success.
    
    ## Parallel Hook Behavior
    - Multiple simultaneously running hooks can be tracked in one live hook
    cell.
    - Adjacent running hooks with the same hook event name and same status
    message collapse into a count, for example `• Running 3 PreToolUse
    hooks: checking command policy`.
    - Running hooks with different event names or different status messages
    remain separate rows.
    
    ## Hook Run Identity
    - `PreToolUse` and `PostToolUse` hook run IDs now include the tool call
    ID which prevents concurrent tool-use hooks from sharing a run ID and
    clobbering each other in the UI.
    - This ID scoping applies to tool-use hooks only; other hook event types
    keep their existing run identity behavior.
    
    ## App-Server Hook Notifications
    - App-server `HookStarted` and `HookCompleted` notifications use the
    same live hook rendering path as core hook events.
    - `UserPromptSubmit` hook notifications now render through the same
    completed hook output format, including warning and stop entries.
  • Add thread title to configurable TUI status line (#17187)
    - Add thread-title as an optional TUI status line item, omitted unless
    the user has set a custom name (`ChatWidget.thread_name`).
    - Refresh the status line when threads are renamded
    - Add snapshot coverage for renamed-thread footer behavior.
  • Queue Realtime V2 response.create while active (#17306)
    Builds on #17264.
    
    - queues Realtime V2 `response.create` while an active response is open,
    then flushes it after `response.done` or `response.cancelled`
    - requests `response.create` after background agent final output and
    steering acknowledgements
    - adds app-server integration coverage for all `response.create` paths
    
    Validation:
    - `just fmt`
    - `cargo check -p codex-app-server --tests`
    - `git diff --check`
    - CI green
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Option to Notify Workspace Owner When Usage Limit is Reached (#16969)
    ## Summary
    - Replace the manual `/notify-owner` flow with an inline confirmation
    prompt when a usage-based workspace member hits a credits-depleted
    limit.
    - Fetch the current workspace role from the live ChatGPT
    `accounts/check/v4-2023-04-27` endpoint so owner/member behavior matches
    the desktop and web clients.
    - Keep owner, member, and spend-cap messaging distinct so we only offer
    the owner nudge when the workspace is actually out of credits.
    
    ## What Changed
    - `backend-client`
    - Added a typed fetch for the current account role from
    `accounts/check`.
      - Mapped backend role values into a Rust workspace-role enum.
    - `app-server` and protocol
      - Added `workspaceRole` to `account/read` and `account/updated`.
    - Derived `isWorkspaceOwner` from the live role, with a fallback to the
    cached token claim when the role fetch is unavailable.
    - `tui`
      - Removed the explicit `/notify-owner` slash command.
    - When a member is blocked because the workspace is out of credits, the
    error now prompts:
    - `Your workspace is out of credits. Request more from your workspace
    owner? [y/N]`
      - Choosing `y` sends the existing owner-notification request.
    - Choosing `n`, pressing `Esc`, or accepting the default selection
    dismisses the prompt without sending anything.
    - Selection popups now honor explicit item shortcuts, which is how the
    `y` / `n` interaction is wired.
    
    ## Reviewer Notes
    - The main behavior change is scoped to usage-based workspace members
    whose workspace credits are depleted.
    - Spend-cap reached should not show the owner-notification prompt.
    - Owners and admins should continue to see `/usage` guidance instead of
    the member prompt.
    - The live role fetch is best-effort; if it fails, we fall back to the
    existing token-derived ownership signal.
    
    ## Testing
    - Manual verification
      - Workspace owner does not see the member prompt.
    - Workspace member with depleted credits sees the confirmation prompt
    and can send the nudge with `y`.
    - Workspace member with spend cap reached does not see the
    owner-notification prompt.
    
    ### Workspace member out of usage
    
    https://github.com/user-attachments/assets/341ac396-eff4-4a7f-bf0c-60660becbea1
    
    ### Workspace owner
    <img width="1728" height="1086" alt="Screenshot 2026-04-09 at 11 48
    22 AM"
    src="https://github.com/user-attachments/assets/06262a45-e3fc-4cc4-8326-1cbedad46ed6"
    />
  • feat(tui): Ctrl+O copy hotkey and harden copy-as-markdown behavior (#16966)
    ## TL;DR
    
    - New `Ctrl+O` shortcut on top of the existing `/copy` command, allowing
    users to copy the latest agent response without having to cancel a plan
    or type `/copy`
    - Copy server clipboard to the client over SSH (OSC 52)
    - Fixes linux copy behavior: a clipboard handle has to be kept alive
    while the paste happens for the contents to be preserved
    - Uses arboard as primary mechanism on Windows, falling back to
    PowerShell copy clipboard function
    - Works with resumes, rolling back during a session, etc.
    
    Tested on macOS, Linux/X11, Windows WSL2, Windows cmd.exe, Windows
    PowerShell, Windows VSCode PowerShell, Windows VSCode WSL2, SSH (macOS
    -> macOS).
    
    ## Problem
    
    The TUI's `/copy` command was fragile. It relied on a single
    `last_copyable_output` field that was bluntly cleared on every rollback
    and thread reconfiguration, making copied content unavailable after
    common operations like backtracking. It also had no keyboard shortcut,
    requiring users to type `/copy` each time. The previous clipboard
    backend mixed platform selection policy with low-level I/O in a way that
    was hard to test, and it did not keep the Linux clipboard owner alive —
    meaning pasted content could vanish once the process that wrote it
    dropped its `arboard::Clipboard`.
    
    This addresses the text-copy failure modes reported in #12836, #15452,
    and #15663: native Linux clipboard access failing in remote or
    unreachable-display environments, copy state going blank even after
    visible assistant output, and local Linux X11 reporting success while
    leaving the clipboard empty.
    
    ## Shortcut rationale
    
    The copy hotkey is `Ctrl+O` rather than `Alt+C` because Alt/Option
    combinations are not delivered consistently by macOS terminal emulators.
    Terminal.app and iTerm2 can treat Option as text input or as a
    configurable Meta/Esc prefix, and Option+C may be consumed or
    transformed before the TUI sees an `Alt+C` key event. `Ctrl+O` is a
    stable control-key chord in Terminal.app, iTerm2, SSH, and the existing
    cross-platform terminal stack.
    
    ## Mental model
    
    Agent responses are now tracked as a bounded, ordinal-indexed history
    (`agent_turn_markdowns: Vec<AgentTurnMarkdown>`) rather than a single
    nullable string. Each completed agent turn appends an entry keyed by its
    ordinal (the number of user turns seen so far). Rollbacks pop entries
    whose ordinal exceeds the remaining turn count, then use the visible
    transcript cells as a best-effort fallback if the ordinal history no
    longer has a surviving entry. This means `/copy` and `Ctrl+O` reflect
    the most recent surviving agent response after a backtrack, instead of
    going blank.
    
    The clipboard backend was rewritten as `clipboard_copy.rs` with a
    strategy-injection design: `copy_to_clipboard_with` accepts closures for
    the OSC 52, arboard, and WSL PowerShell paths, making the selection
    logic fully unit-testable without touching real clipboards. On Linux,
    the `Clipboard` handle is returned as a `ClipboardLease` stored on
    `ChatWidget`, keeping X11/Wayland clipboard ownership alive for the
    lifetime of the TUI. When native copy fails under WSL, the backend now
    tries the Windows clipboard through PowerShell before falling back to
    OSC 52.
    
    ## Non-goals
    
    - This change does not introduce rich-text (HTML) clipboard support; the
    copied content is raw markdown.
    - It does not add a paste-from-history picker or multi-entry clipboard
    ring.
    - WSL support remains a best-effort fallback, not a new configuration
    surface or guarantee for every terminal/host combination.
    
    ## Tradeoffs
    
    - **Bounded history (256 entries)**: `MAX_AGENT_COPY_HISTORY` caps
    memory. For sessions with thousands of turns this silently drops the
    oldest entries. The cap is generous enough for realistic sessions.
    - **`saw_copy_source_this_turn` flag**: Prevents double-recording when
    both `AgentMessage` and `TurnComplete.last_agent_message` fire for the
    same turn. The flag is reset on turn start and on turn complete,
    creating a narrow window where a race between the two events could
    theoretically skip recording. In practice the protocol delivers them
    sequentially.
    - **Transcript fallback on rollback**:
    `last_agent_markdown_from_transcript` walks the visible transcript cells
    to reconstruct plain text when the ordinal history has been fully
    truncated. This path uses `AgentMessageCell::plain_text()` which joins
    rendered spans, so it reconstructs display text rather than the original
    raw markdown. It keeps visible text copyable after rollback, but
    responses with markdown-specific syntax can diverge from the original
    source.
    - **Clipboard fallback ordering**: SSH still uses OSC 52 exclusively
    because native/PowerShell clipboard access would target the wrong
    machine. Local sessions try native clipboard first, then WSL PowerShell
    when running under WSL, then OSC 52. This adds one process-spawn
    fallback for WSL users but keeps the normal desktop and SSH paths
    simple.
    
    ## Architecture
    
    ```
    chatwidget.rs
    ├── agent_turn_markdowns: Vec<AgentTurnMarkdown>  // ordinal-indexed history
    ├── last_agent_markdown: Option<String>            // always == last entry's markdown
    ├── completed_turn_count: usize                    // incremented when user turns enter history
    ├── saw_copy_source_this_turn: bool                // dedup guard
    ├── clipboard_lease: Option<ClipboardLease>        // keeps Linux clipboard owner alive
    │
    ├── record_agent_markdown(&str)                    // append/update history entry
    ├── truncate_agent_turn_markdowns_to_turn_count()  // rollback support
    ├── copy_last_agent_markdown()                     // public entry point (slash + hotkey)
    └── copy_last_agent_markdown_with(fn)              // testable core
    
    clipboard_copy.rs
    ├── copy_to_clipboard(text) -> Result<Option<ClipboardLease>>
    ├── copy_to_clipboard_with(text, ssh, wsl, osc52_fn, arboard_fn, wsl_fn)
    ├── ClipboardLease { _clipboard on linux }
    ├── arboard_copy(text)          // platform-conditional native clipboard path
    ├── wsl_clipboard_copy(text)    // WSL PowerShell fallback
    ├── osc52_copy(text)            // /dev/tty -> stdout fallback
    ├── SuppressStderr              // macOS stderr redirect guard
    ├── is_ssh_session()
    └── is_wsl_session()
    
    app_backtrack.rs
    ├── last_agent_markdown_from_transcript()  // reconstruct from visible cells
    └── truncate call sites in trim/apply_confirmed_rollback
    ```
    
    ## Observability
    
    - `tracing::warn!` on native clipboard failure before OSC 52 fallback.
    - `tracing::debug!` on `/dev/tty` open/write failure before stdout
    fallback.
    - History cell messages: "Copied last message to clipboard", "Copy
    failed: {error}", "No agent response to copy" appear in the TUI
    transcript.
    
    ## Tests
    
    - `clipboard_copy.rs`: Unit tests cover OSC 52 encoding roundtrip,
    payload size rejection, writer output, SSH-only OSC52 routing, non-WSL
    native-to-OSC52 fallback, WSL native-to-PowerShell fallback, WSL
    PowerShell-to-OSC52 fallback, and all-error reporting via strategy
    injection.
    - `chatwidget/tests/slash_commands.rs`: Updated existing `/copy` tests
    to use `last_agent_markdown_text()` accessor. Added coverage for the
    Linux clipboard lease lifecycle, missing
    `TurnComplete.last_agent_message` fallback through completed assistant
    items, replayed legacy agent messages, stale-output prevention after
    rollback, and the `Ctrl+O` no-output hotkey path.
    - `app_backtrack.rs`: Added
    `agent_group_count_ignores_context_compacted_marker` verifying that
    info-event cells don't inflate the agent group count.
    
    ---------
    
    Co-authored-by: Felipe Coury <felipe.coury@gmail.com>
    Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  • Forward app-server turn clientMetadata to Responses (#16009)
    ## Summary
    App-server v2 already receives turn-scoped `clientMetadata`, but the
    Rust app-server was dropping it before the outbound Responses request.
    This change keeps the fix lightweight by threading that metadata through
    the existing turn-metadata path rather than inventing a new transport.
    
    ## What we're trying to do and why
    We want turn-scoped metadata from the app-server protocol layer,
    especially fields like Hermes/GAAS run IDs, to survive all the way to
    the actual Responses API request so it is visible in downstream
    websocket request logging and analytics.
    
    The specific bug was:
    - app-server protocol uses camelCase `clientMetadata`
    - Responses transport already has an existing turn metadata carrier:
    `x-codex-turn-metadata`
    - websocket transport already rewrites that header into
    `request.request_body.client_metadata["x-codex-turn-metadata"]`
    - but the Rust app-server never parsed or stored `clientMetadata`, so
    nothing from the app-server request was making it into that existing
    path
    
    This PR fixes that without adding a new header or a second metadata
    channel.
    
    ## How we did it
    ### Protocol surface
    - Add optional `clientMetadata` to v2 `TurnStartParams` and
    `TurnSteerParams`
    - Regenerate the JSON schema / TypeScript fixtures
    - Update app-server docs to describe the field and its behavior
    
    ### Runtime plumbing
    - Add a dedicated core op for app-server user input carrying turn-scoped
    metadata: `Op::UserInputWithClientMetadata`
    - Wire `turn/start` and `turn/steer` through that op / signature path
    instead of dropping the metadata at the message-processor boundary
    - Store the metadata in `TurnMetadataState`
    
    ### Transport behavior
    - Reuse the existing serialized `x-codex-turn-metadata` payload
    - Merge the new app-server `clientMetadata` into that JSON additively
    - Do **not** replace built-in reserved fields already present in the
    turn metadata payload
    - Keep websocket behavior unchanged at the outer shape level: it still
    sends only `client_metadata["x-codex-turn-metadata"]`, but that JSON
    string now contains the merged fields
    - Keep HTTP fallback behavior unchanged except that the existing
    `x-codex-turn-metadata` header now includes the merged fields too
    
    ### Request shape before / after
    Before, a websocket `response.create` looked like:
    ```json
    {
      "type": "response.create",
      "client_metadata": {
        "x-codex-turn-metadata": "{\"session_id\":\"...\",\"turn_id\":\"...\"}"
      }
    }
    ```
    Even if the app-server caller supplied `clientMetadata`, it was not
    represented there.
    
    After, the same request shape is preserved, but the serialized payload
    now includes the new turn-scoped fields:
    ```json
    {
      "type": "response.create",
      "client_metadata": {
        "x-codex-turn-metadata": "{\"session_id\":\"...\",\"turn_id\":\"...\",\"fiber_run_id\":\"fiber-start-123\",\"origin\":\"gaas\"}"
      }
    }
    ```
    
    ## Validation
    ### Targeted tests added / updated
    - protocol round-trip coverage for `clientMetadata` on `turn/start` and
    `turn/steer`
    - protocol round-trip coverage for `Op::UserInputWithClientMetadata`
    - `TurnMetadataState` merge test proving client metadata is added
    without overwriting reserved built-in fields
    - websocket request-shape test proving outbound `response.create`
    contains merged metadata inside
    `client_metadata["x-codex-turn-metadata"]`
    - app-server integration tests proving:
    - `turn/start` forwards `clientMetadata` into the outbound Responses
    request path
      - websocket warmup + real turn request both behave correctly
      - `turn/steer` updates the follow-up request metadata
    
    ### Commands run
    - `just write-app-server-schema`
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-protocol`
    - `cargo test -p codex-core
    turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields
    --lib`
    - `cargo test -p codex-core --test all
    responses_websocket_preserves_custom_turn_metadata_fields`
    - `cargo test -p codex-app-server --test all client_metadata`
    - `cargo test -p codex-app-server --test all
    turn_start_forwards_client_metadata_to_responses_websocket_request_body_v2
    -- --nocapture`
    - `just fmt`
    - `just fix -p codex-core -p codex-protocol -p codex-app-server-protocol
    -p codex-app-server`
    - `just fix -p codex-exec -p codex-tui-app-server`
    - `just argument-comment-lint`
    
    ### Full suite note
    `cargo test` in `codex-rs` still fails in:
    -
    `suite::v2::turn_interrupt::turn_interrupt_resolves_pending_command_approval_request`
    
    I verified that same failure on a clean detached `HEAD` worktree with an
    isolated `CARGO_TARGET_DIR`, so it is not caused by this patch.
  • [codex] Show ctrl + t hint on truncated exec output in TUI (#17076)
    ## What
    
    Show an inline `ctrl + t to view transcript` hint when exec output is
    truncated in the main TUI chat view.
    
    ## Why
    
    Today, truncated exec output shows `… +N lines`, but it does not tell
    users that the full content is already available through the existing
    transcript overlay. That makes hidden output feel lost instead of
    discoverable.
    
    This change closes that discoverability gap without introducing a new
    interaction model.
    
    Fixes: CLI-5740
    
    ## How
    
    - added an output-specific truncation hint in `ExecCell` rendering
    - applied that hint in both exec-output truncation paths:
      - logical head/tail truncation before wrapping
      - row-budget truncation after wrapping
    - preserved the existing row-budget behavior on narrow terminals by
    reserving space for the longer hint line
    - updated the relevant snapshot and added targeted regression coverage
    
    ## Intentional design decisions
    
    - **Aligned shortcut styling with the visible footer UI**  
    The inline hint uses `ctrl + t`, not `Ctrl+T`, to match the TUI’s
    rendered key-hint style.
    
    - **Kept the noun `transcript`**  
    The product already exposes this flow as the transcript overlay, so the
    hint points at the existing concept instead of inventing a new label.
    
    - **Preserved narrow-terminal behavior**  
    The longer hint text is accounted for in the row-budget truncation path
    so the visible output still respects the existing viewport cap.
    
    - **Did not add the hint to long command truncation**  
    This PR only changes hidden **output** truncation. Long command
    truncation still uses the plain ellipsis form because `ctrl + t` is not
    the same kind of “show hidden output” escape hatch there.
    
    - **Did not widen scope to other truncation surfaces**  
    This does not change MCP/tool-call truncation in `history_cell.rs`, and
    it does not change transcript-overlay behavior itself.
    
    ## Validation
    
    ### Automated
    - `just fmt`
    - `cargo test -p codex-tui`
    
    ### Manual
    - ran `just tui-with-exec-server`
    - executed `!seq 1 200`
    - confirmed the main view showed the new `ctrl + t to view transcript`
    truncation hint
    - pressed `ctrl + t` and confirmed the transcript overlay still exposed
    the full output
    - closed the overlay and returned to the main view
    
    ## Visual proof
    
    Screenshot/video attached in the PR UI showing:
    - the truncated exec output row with the new hint
    - the transcript overlay after `ctrl + t`
  • chore: merge name and title (#17116)
    Merge title and name concept to leverage the sqlite title column and
    have more efficient queries
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Render statusline context as a meter (#17170)
    Problem: The statusline reported context as an “X% left” value, which
    could be mistaken for quota, and context usage was included in the
    default footer.
    
    Solution: Render configured context status items as a filling context
    meter, preserve `context-used` as a legacy alias while hiding it from
    the setup menu, and remove context from the default statusline. It will
    still be available as an opt-in option for users who want to see it.
    
    <img width="317" height="39" alt="image"
    src="https://github.com/user-attachments/assets/3aeb39bb-f80d-471f-88fe-d55e25b31491"
    />
  • feat: advanced announcements per OS and plans (#17226)
    Support things like
    ```
      [[announcements]]
      content = "custom message"
      from_date = "2026-04-09"
      to_date = "2026-06-01"
      target_app = "cli"
      target_plan_types = ["pro"]
      target_oses = ["macos"]
      version_regex = "..." # add version of the patch
      ```
  • feat: /resume per ID/name (#17222)
    Support `/resume 00000-0000-0000-00000000` from the TUI (equivalent for
    the name)
  • Skip update prompts for source builds (#17186)
    Addresses #17166
    
    Problem: Source builds report version 0.0.0, so the TUI update path can
    treat any released Codex version as upgradeable and show startup or
    popup prompts.
    
    Solution: Skip both TUI update prompt entry points when the running CLI
    version is the source-build sentinel 0.0.0.
  • Add TUI notification condition config (#17175)
    Problem: TUI desktop notifications are hard-gated on terminal focus, so
    terminal/IDE hosts that want in-focus notifications cannot opt in.
    
    Solution: Add a flat `[tui] notification_condition` setting (`unfocused`
    by default, `always` opt-in), carry grouped TUI notification settings
    through runtime config, apply method + condition together in the TUI,
    and regenerate the config schema.
  • Add realtime voice selection (#17176)
    - Add realtime voice selection for realtime/start.
    - Expose the supported v1/v2 voice lists and cover explicit, configured,
    default, and invalid voice paths.
  • Move default realtime prompt into core (#17165)
    - Adds a core-owned realtime backend prompt template and preparation
    path.
    - Makes omitted realtime start prompts use the core default, while null
    or empty prompts intentionally send empty instructions.
    - Covers the core realtime path and app-server v2 path with integration
    coverage.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>