Commit Graph

15 Commits

  • 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>
  • Support Codex Apps auth elicitations (#19193)
    ## Summary
    
    - request URL-mode MCP elicitations when Codex Apps tool calls fail with
    connector auth metadata
    - route Codex Apps auth URL elicitations into the TUI app-link flow
    
    ## Test plan
    
    - `just fmt`
    - `cargo test -p codex-core mcp_tool_call::tests`
    - `cargo test -p codex-mcp`
    - `cargo test -p codex-tui bottom_pane::app_link_view::tests`
    - `just fix -p codex-core`
    - `just fix -p codex-mcp`
    - `just fix -p codex-tui`
    
    Also attempted broader local runs:
    
    - `cargo test -p codex-core` fails in unrelated
    config/request-permission/proxy-sensitive tests under the current Codex
    Desktop environment.
    - `cargo test -p codex-tui` fails in unrelated status
    snapshots/trust-default tests because the ambient environment renders
    workspace-write/network permission defaults.
  • 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.
  • 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.
  • Keep paused goals paused on thread resume (#20790)
    ## Summary
    
    Early adopters of the `/goal` feature have provided feedback that they
    expect a goal they explicitly paused to remain paused when they resume a
    thread. Previously, resuming a thread would reactivate a paused goal.
    
    This PR keeps persisted goal status unchanged during thread resume. This
    honors the user feedback while also simplifying the core goal logic.
    
    Rather than have the core logic automatically resume a paused goal, that
    responsibility is transferred to the client. The TUI now detects a
    resumed thread with a paused goal and asks the user whether to `Resume
    goal` or `Leave paused`. The prompt appears only for quiet resume flows,
    so users who resume with an immediate prompt are not interrupted.
    
    <img width="544" height="111" alt="image"
    src="https://github.com/user-attachments/assets/0ac9de1c-6ee6-47ba-b223-c03c8eb4c192"
    />
  • 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`
  • 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`
  • 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
  • Render delegated patch approval details (#19709)
    ## Why
    
    Fixes #19632.
    
    When a delegated agent requests approval for an in-progress file change,
    the parent TUI handles that request from an inactive thread. The app
    server already sent the `FileChange` item with the proposed diff, but
    the inactive-thread approval path was not recovering and rendering it
    the same way as the active-thread path.
    
    The result was an inconsistent approval prompt: main-thread edits show a
    normal patch preview history item before the approval modal, while
    delegated edits did not show that preview in the transcript flow.
    
    ## What Changed
    
    - Recover buffered or historical `FileChange` item changes when building
    inactive-thread file-change approval requests.
    - Reuse the app-server file-change conversion helper for both live
    transcript rendering and inactive-thread approvals.
    - Render recovered delegated patches as a normal patch preview history
    cell before the approval modal.
    - Keep apply-patch approval modals focused on the decision prompt and
    optional metadata; they do not render a synthetic command line or embed
    the diff body.
    
    ## Manual Repro And Verification
    
    I manually reproduced the issue using a file under `~/Desktop` so the
    write would require approval.
    
    Before the fix:
    
    1. Ask the main thread: `Use apply_patch, not shell redirection or
    Python, to create ~/Desktop/bug1.txt with three short lines.`
    2. Observe the expected TUI shape: the transcript shows a normal patch
    preview such as `• Added ~/Desktop/bug1.txt (+N -0)` above the approval
    modal, and the modal contains only the approval prompt/options without a
    synthetic command line.
    3. Ask for the delegated path: `Spawn a worker. Have it use apply_patch,
    not shell redirection or Python, to create ~/Desktop/bug1.txt with four
    short lines.`
    4. Observe the delegated approval is inconsistent: the parent view does
    not render the proposed patch as the normal transcript preview before
    the modal, so the diff context is missing from the stream or appears
    inside the modal instead of in the history flow.
    
    After the fix:
    
    1. Repeat the delegated worker prompt with `apply_patch`.
    2. Confirm the parent view renders the same normal patch preview history
    cell (`• Added ~/Desktop/bug1.txt (+N -0)` plus the diff) immediately
    before the approval modal.
    3. Confirm the approval modal remains focused on the decision prompt.
    For delegated approvals it may show the worker thread label, but it
    should not show a `$ apply_patch` command line or embed the diff body in
    the modal.
  • 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
  • tui: carry permission profiles on user turns (#18285)
    ## Why
    
    Per-turn permission overrides should use the same canonical profile
    abstraction as session configuration. That lets TUI submissions preserve
    exact configured permissions without round-tripping through legacy
    sandbox fields.
    
    ## What changed
    
    This adds `permission_profile` to user-turn operations, threads it
    through TUI/app-server submission paths, fills the new field in existing
    test fixtures, and adds coverage that composer submission includes the
    configured profile.
    
    ## Verification
    
    - `cargo test -p codex-tui permissions -- --nocapture`
    - `cargo test -p codex-core --test all permissions_messages --
    --nocapture`
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/18285).
    * #18288
    * #18287
    * #18286
    * __->__ #18285
  • tui: sync session permission profiles (#18284)
    ## Why
    
    Once `SessionConfigured` carries the active `PermissionProfile`, the TUI
    must treat that as authoritative session state. Otherwise the widget can
    keep stale local permission details after a session is configured or
    resumed.
    
    The TUI also keeps a local `Config` copy used for later operations, so
    session-sourced profiles and subsequent local sandbox changes need to
    keep the derived split runtime permissions in sync. Because this PR may
    land before the follow-up user-turn profile plumbing, embedded
    app-server turns also need a standalone path for carrying local runtime
    sandbox overrides.
    
    ## What changed
    
    - Sync the chat widget runtime filesystem/network permissions from
    `SessionConfigured.permission_profile`, with the legacy `sandbox_policy`
    as the fallback.
    - Recompute split runtime permissions whenever the TUI applies or
    carries forward a local sandbox-policy override.
    - Mark feature-driven Auto-review sandbox changes as runtime sandbox
    overrides so the standalone embedded turn-start profile path is used
    even without the follow-up user-turn profile PR.
    - Send a turn-start `permissionProfile` for embedded,
    non-ExternalSandbox turns when the TUI has a runtime sandbox override;
    remote and ExternalSandbox turns keep using the legacy sandbox field.
    - Extend coverage for profile sync, local sandbox changes,
    ExternalSandbox fallback, feature-driven sandbox overrides, and
    turn-start permission override selection.
    
    ## Verification
    
    - `cargo test -p codex-tui
    update_feature_flags_enabling_guardian_selects_auto_review`
    - `cargo test -p codex-tui
    turn_start_permission_overrides_send_profiles_only_for_embedded_runtime_overrides`
    - `cargo test -p codex-tui permission_settings_sync`
    - `cargo test -p codex-tui
    session_configured_external_sandbox_keeps_external_runtime_policy`
    - `cargo test -p codex-tui
    session_configured_syncs_widget_config_permissions_and_cwd`
    - `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/18284).
    * #18288
    * #18287
    * #18286
    * #18285
    * __->__ #18284
  • Add plumbing to approve stored Auto-Review denials (#18955)
    ## Summary
    
    This adds the structural plumbing needed for an app-server client to
    approve a previously denied Guardian review and carry that approval
    context into the next model turn.
    
    This PR does not add the actual `/auto-review-denials` tool 
    
    ## What Changed
    
    - Added app-server v2 RPC `thread/approveGuardianDeniedAction`.
    - Added generated JSON schema and TypeScript fixtures for
    `ThreadApproveGuardianDeniedAction*`.
    - Added core `Op::ApproveGuardianDeniedAction`.
    - Added a core handler that validates the event is a denied Guardian
    assessment and injects a developer message containing the stored denial
    event JSON.
    - Queues the approval context for the next turn if there is no active
    turn yet.
    - Added the TUI app-server bridge so `Op::ApproveGuardianDeniedAction {
    event }` is routed to the app-server request.
    
    ## What This Does Not Do
    
    - Does not add `/auto-review-denials`.
    - Does not add chat widget recent-denial state.
    - Does not add popup/list UI.
    - Does not add a product-facing denial lookup/store.
    - Does not change where Guardian denials are originally emitted or
    persisted.
    
    ## Verification
    
    - `cargo test -p codex-tui thread_approve_guardian_denied_action`
  • 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`.