Commit Graph

977 Commits

  • Override truncation policy at model info level (#8856)
    We used to override truncation policy by comparing model info vs config
    value in context manager. A better way to do it is to construct model
    info using the config value
  • fix: implement 'Allow this session' for apply_patch approvals (#8451)
    **Summary**
    This PR makes “ApprovalDecision::AcceptForSession / don’t ask again this
    session” actually work for `apply_patch` approvals by caching approvals
    based on absolute file paths in codex-core, properly wiring it through
    app-server v2, and exposing the choice in both TUI and TUI2.
    - This brings `apply_patch` calls to be at feature-parity with general
    shell commands, which also have a "Yes, and don't ask again" option.
    - This also fixes VSCE's "Allow this session" button to actually work.
    
    While we're at it, also split the app-server v2 protocol's
    `ApprovalDecision` enum so execpolicy amendments are only available for
    command execution approvals.
    
    **Key changes**
    - Core: per-session patch approval allowlist keyed by absolute file
    paths
    - Handles multi-file patches and renames/moves by recording both source
    and destination paths for `Update { move_path: Some(...) }`.
    - Extend the `Approvable` trait and `ApplyPatchRuntime` to work with
    multiple keys, because an `apply_patch` tool call can modify multiple
    files. For a request to be auto-approved, we will need to check that all
    file paths have been approved previously.
    - App-server v2: honor AcceptForSession for file changes
    - File-change approval responses now map AcceptForSession to
    ReviewDecision::ApprovedForSession (no longer downgraded to plain
    Approved).
    - Replace `ApprovalDecision` with two enums:
    `CommandExecutionApprovalDecision` and `FileChangeApprovalDecision`
    - TUI / TUI2: expose “don’t ask again for these files this session”
    - Patch approval overlays now include a third option (“Yes, and don’t
    ask again for these files this session (s)”).
        - Snapshot updates for the approval modal.
    
    **Tests added/updated**
    - Core:
    - Integration test that proves ApprovedForSession on a patch skips the
    next patch prompt for the same file
    - App-server:
    - v2 integration test verifying
    FileChangeApprovalDecision::AcceptForSession works properly
    
    **User-visible behavior**
    - When the user approves a patch “for session”, future patches touching
    only those previously approved file(s) will no longer prompt gain during
    that session (both via app-server v2 and TUI/TUI2).
    
    **Manual testing**
    Tested both TUI and TUI2 - see screenshots below.
    
    TUI:
    <img width="1082" height="355" alt="image"
    src="https://github.com/user-attachments/assets/adcf45ad-d428-498d-92fc-1a0a420878d9"
    />
    
    
    TUI2:
    <img width="1089" height="438" alt="image"
    src="https://github.com/user-attachments/assets/dd768b1a-2f5f-4bd6-98fd-e52c1d3abd9e"
    />
  • remove unnecessary todos (#8842)
    > // todo(aibrahim): why are we passing model here while it can change?
    
    we update it on each turn with `.with_model`
    
    > //TODO(aibrahim): run CI in release mode.
    
    although it's good to have, release builds take double the time tests
    take.
    
    > // todo(aibrahim): make this async function
    
    we figured out another way of doing this sync
  • Merge Modelfamily into modelinfo (#8763)
    - Merge ModelFamily into ModelInfo
    - Remove logic for adding instructions to apply patch
    - Add compaction limit and visible context window to `ModelInfo`
  • Move tests below auth manager (#8840)
    To simplify future diffs
  • chore: unify conversation with thread name (#8830)
    Done and verified by Codex + refactor feature of RustRover
  • Enable model upgrade popup even when selected model is no longer in picker (#8802)
    With `config.toml`:
    ```
    model = "gpt-5.1-codex"
    ```
    (where `gpt-5.1-codex` has `show_in_picker: false` in
    [`model_presets.rs`](https://github.com/openai/codex/blob/main/codex-rs/core/src/models_manager/model_presets.rs);
    this happens if the user hasn't used codex in a while so they didn't see
    the popup before their model was changed to `show_in_picker: false`)
    
    The upgrade picker used to not show (because `gpt-5.1-codex` was
    filtered out of the model list in code). Now, the filtering is done
    downstream in tui and app-server, so the model upgrade popup shows:
    
    <img width="1503" height="227" alt="Screenshot 2026-01-06 at 5 04 37 PM"
    src="https://github.com/user-attachments/assets/26144cc2-0b3f-4674-ac17-e476781ec548"
    />
  • add web_search_cached flag (#8795)
    Add `web_search_cached` feature to config. Enables `web_search` tool
    with access only to cached/indexed results (see
    [docs](https://platform.openai.com/docs/guides/tools-web-search#live-internet-access)).
    
    This takes precedence over the existing `web_search_request`, which
    continues to enable `web_search` over live results as it did before.
    
    `web_search_cached` is disabled for review mode, as `web_search_request`
    is.
  • feat(app-server): thread/rollback API (#8454)
    Add `thread/rollback` to app-server to support IDEs undo-ing the last N
    turns of a thread.
    
    For context, an IDE partner will be supporting an "undo" capability
    where the IDE (the app-server client) will be responsible for reverting
    the local changes made during the last turn. To support this well, we
    also need a way to drop the last turn (or more generally, the last N
    turns) from the agent's context. This is what `thread/rollback` does.
    
    **Core idea**: A Thread rollback is represented as a persisted event
    message (EventMsg::ThreadRollback) in the rollout JSONL file, not by
    rewriting history. On resume, both the model's context (core replay) and
    the UI turn list (app-server v2's thread history builder) apply these
    markers so the pruned history is consistent across live conversations
    and `thread/resume`.
    
    Implementation notes:
    - Rollback only affects agent context and appends to the rollout file;
    clients are responsible for reverting files on disk.
    - If a thread rollback is currently in progress, subsequent
    `thread/rollback` calls are rejected.
    - Because we use `CodexConversation::submit` and codex core tracks
    active turns, returning an error on concurrent rollbacks is communicated
    via an `EventMsg::Error` with a new variant
    `CodexErrorInfo::ThreadRollbackFailed`. app-server watches for that and
    sends the BAD_REQUEST RPC response.
    
    Tests cover thread rollbacks in both core and app-server, including when
    `num_turns` > existing turns (which clears all turns).
    
    **Note**: this explicitly does **not** behave like `/undo` which we just
    removed from the CLI, which does the opposite of what `thread/rollback`
    does. `/undo` reverts local changes via ghost commits/snapshots and does
    not modify the agent's context / conversation history.
  • feat: agent controller (#8783)
    Added an agent control plane that lets sessions spawn or message other
    conversations via `AgentControl`.
    
    `AgentBus` (core/src/agent/bus.rs) keeps track of the last known status
    of a conversation.
    
    ConversationManager now holds shared state behind an Arc so AgentControl
    keeps only a weak back-reference, the goal is just to avoid explicit
    cycle reference.
    
    Follow-ups:
    * Build a small tool in the TUI to be able to see every agent and send
    manual message to each of them
    * Handle approval requests in this TUI
    * Add tools to spawn/communicate between agents (see related design)
    * Define agent types
  • Use ConfigLayerStack for skills discovery. (#8497)
    Use ConfigLayerStack to get all folders while loading skills.
  • feat: add justification arg to prefix_rule() in *.rules (#8751)
    Adds an optional `justification` parameter to the `prefix_rule()`
    execpolicy DSL so policy authors can attach human-readable rationale to
    a rule. That justification is propagated through parsing/matching and
    can be surfaced to the model (or approval UI) when a command is blocked
    or requires approval.
    
    When a command is rejected (or gated behind approval) due to policy, a
    generic message makes it hard for the model/user to understand what went
    wrong and what to do instead. Allowing policy authors to supply a short
    justification improves debuggability and helps guide the model toward
    compliant alternatives.
    
    Example:
    
    ```python
    prefix_rule(
        pattern = ["git", "push"],
        decision = "forbidden",
        justification = "pushing is blocked in this repo",
    )
    ```
    
    If Codex tried to run `git push origin main`, now the failure would
    include:
    
    ```
    `git push origin main` rejected: pushing is blocked in this repo
    ```
    
    whereas previously, all it was told was:
    
    ```
    execpolicy forbids this command
    ```
  • chore: improve skills render section (#8459)
    This change improves the skills render section
    - Separate the skills list from usage rules with clear subheadings
    - Define skill more clearly upfront
    - Remove confusing trigger/discovery wording and make reference-following guidance more actionable
  • feat: expose outputSchema to user_turn/turn_start app_server API (#8377)
    What changed
    - Added `outputSchema` support to the app-server APIs, mirroring `codex
    exec --output-schema` behavior.
    - V1 `sendUserTurn` now accepts `outputSchema` and constrains the final
    assistant message for that turn.
    - V2 `turn/start` now accepts `outputSchema` and constrains the final
    assistant message for that turn (explicitly per-turn only).
    
    Core behavior
    - `Op::UserTurn` already supported `final_output_json_schema`; now V1
    `sendUserTurn` forwards `outputSchema` into that field.
    - `Op::UserInput` now carries `final_output_json_schema` for per-turn
    settings updates; core maps it into
    `SessionSettingsUpdate.final_output_json_schema` so it applies to the
    created turn context.
    - V2 `turn/start` does NOT persist the schema via `OverrideTurnContext`
    (it’s applied only for the current turn). Other overrides
    (cwd/model/etc) keep their existing persistent behavior.
    
    API / docs
    - `codex-rs/app-server-protocol/src/protocol/v1.rs`: add `output_schema:
    Option<serde_json::Value>` to `SendUserTurnParams` (serialized as
    `outputSchema`).
    - `codex-rs/app-server-protocol/src/protocol/v2.rs`: add `output_schema:
    Option<JsonValue>` to `TurnStartParams` (serialized as `outputSchema`).
    - `codex-rs/app-server/README.md`: document `outputSchema` for
    `turn/start` and clarify it applies only to the current turn.
    - `codex-rs/docs/codex_mcp_interface.md`: document `outputSchema` for v1
    `sendUserTurn` and v2 `turn/start`.
    
    Tests added/updated
    - New app-server integration tests asserting `outputSchema` is forwarded
    into outbound `/responses` requests as `text.format`:
      - `codex-rs/app-server/tests/suite/output_schema.rs`
      - `codex-rs/app-server/tests/suite/v2/output_schema.rs`
    - Added per-turn semantics tests (schema does not leak to the next
    turn):
      - `send_user_turn_output_schema_is_per_turn_v1`
      - `turn_start_output_schema_is_per_turn_v2`
    - Added protocol wire-compat tests for the merged op:
      - serialize omits `final_output_json_schema` when `None`
      - deserialize works when field is missing
      - serialize includes `final_output_json_schema` when `Some(schema)`
    
    Call site updates (high level)
    - Updated all `Op::UserInput { .. }` constructions to include
    `final_output_json_schema`:
      - `codex-rs/app-server/src/codex_message_processor.rs`
      - `codex-rs/core/src/codex_delegate.rs`
      - `codex-rs/mcp-server/src/codex_tool_runner.rs`
      - `codex-rs/tui/src/chatwidget.rs`
      - `codex-rs/tui2/src/chatwidget.rs`
      - plus impacted core tests.
    
    Validation
    - `just fmt`
    - `cargo test -p codex-core`
    - `cargo test -p codex-app-server`
    - `cargo test -p codex-mcp-server`
    - `cargo test -p codex-tui`
    - `cargo test -p codex-tui2`
    - `cargo test -p codex-protocol`
    - `cargo clippy --all-features --tests --profile dev --fix -- -D
    warnings`
  • (MacOS) Load config requirements from MDM (#8743)
    Load managed requirements from MDM key `requirements_toml_base64`.
    
    Tested on my Mac (using `defaults` to set the preference, though this
    would be set by MDM in production):
    
    ```
    ➜  codex git:(gt/mdm-requirements) defaults read com.openai.codex requirements_toml_base64 | base64 -d
    allowed_approval_policies = ["on-request"]
    
    ➜  codex git:(gt/mdm-requirements) just c --yolo
    cargo run --bin codex -- "$@"
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.26s
         Running `target/debug/codex --yolo`
    Error loading configuration: value `Never` is not in the allowed set [OnRequest]
    error: Recipe `codex` failed on line 11 with exit code 1
    
    ➜  codex git:(gt/mdm-requirements) defaults delete com.openai.codex requirements_toml_base64
    
    ➜  codex git:(gt/mdm-requirements) just c --yolo
    cargo run --bin codex -- "$@"
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.24s
         Running `target/debug/codex --yolo`
    ╭──────────────────────────────────────────────────────────╮
    │ >_ OpenAI Codex (v0.0.0)                                 │
    │                                                          │
    │ model:     codex-auto-balanced medium   /model to change │
    │ directory: ~/code/codex/codex-rs                         │
    ╰──────────────────────────────────────────────────────────╯
    
      Tip: Start a fresh idea with /new; the previous session stays in history.
    ```
  • [MCP] Sanitize MCP tool names to ensure they are compatible with the Responses APO (#8694)
    The Responses API requires that all tool names conform to
    '^[a-zA-Z0-9_-]+$'. This PR replaces all non-conforming characters with
    `_` to ensure that they can be used.
    
    Fixes #8174
  • fix: /review to respect session cwd (#8738)
    Fixes /review base-branch prompt resolution to use the session/turn cwd
    (respecting runtime cwd overrides) so merge-base/diff guidance is
    computed from the intended repo; adds a regression test for cwd
    overrides; tested with cargo test -p codex-core --test all
    review_uses_overridden_cwd_for_base_branch_merge_base.
  • Attach more tags to feedback submissions (#8688)
    Attach more tags to sentry feedback so it's easier to classify and debug
    without having to scan through logs.
    
    Formatting isn't amazing but it's a start.
    <img width="1234" height="276" alt="image"
    src="https://github.com/user-attachments/assets/521a349d-f627-4051-b511-9811cd5cd933"
    />
  • hard disable remote models refresh when feature is disabled (#8687)
    We were refreshing on tag mismatch even with feature is disabled
  • Account for last token count on resume (#8677)
    last token count in context manager is initialized to 0. Gets populated
    only on events from server.
    
    This PR populates it on resume so we can decide if we need to compact or
    not.
  • Refresh on models etag mismatch (#8491)
    - Send models etag
    - Refresh models on 412
    - This wires `ModelsManager` to `ModelFamily` so we don't mutate it
    mid-turn
  • [chore] add additional_details to StreamErrorEvent + wire through (#8307)
    ### What
    
    Builds on #8293.
    
    Add `additional_details`, which contains the upstream error message, to
    relevant structures used to pass along retryable `StreamError`s.
    
    Uses the new TUI status indicator's `details` field (shows under the
    status header) to display the `additional_details` error to the user on
    retryable `Reconnecting...` errors. This adds clarity for users for
    retryable errors.
    
    Will make corresponding change to VSCode extension to show
    `additional_details` as expandable from the `Reconnecting...` cell.
    
    Examples:
    <img width="1012" height="326" alt="image"
    src="https://github.com/user-attachments/assets/f35e7e6a-8f5e-4a2f-a764-358101776996"
    />
    
    <img width="1526" height="358" alt="image"
    src="https://github.com/user-attachments/assets/0029cbc0-f062-4233-8650-cc216c7808f0"
    />
  • Remove reasoning format (#8484)
    This isn't very useful parameter. 
    
    logic:
    ```
    if model puts `**` in their reasoning, trim it and visualize the header.
    if couldn't trim: don't render
    if model doesn't support: don't render
    ```
    
    We can simplify to:
    ```
    if could trim, visualize header.
    if not, don't render
    ```
  • fix: limit output size for exec command in unified exec (#8460)
    ### Issue
    [Investigation
    thread](https://openai.slack.com/archives/C095U48JNL9/p1766426234975789)
    
    Github issue: [issue one](https://github.com/openai/codex/issues/8197),
    [issue two](https://github.com/openai/codex/issues/8358), [issue
    three](https://github.com/openai/codex/issues/7585)
    Commonality: working in monorepo and large projects. Multiple threads
    going and showing sluggishness ending with a crash and grey background.
    Potential high usage of context.
    How to reproduce:
    * Open the whole monorepo with cursor or VSC on the latest extension.
    * Run ls -R in current CWD.
    
    ### Change
    In unified exec, we do not have max output check to the delta we ouput
    for shell commands. This causes issue with our VSCE UI which take the
    delta and construct the shell output.
  • chore: save more about turn context in rollout log file (#8458)
    ### Motivation
    - Persist richer per-turn configuration in rollouts so resumed/forked
    sessions and tooling can reason about the exact instruction inputs and
    output constraints used for a turn.
    
    ### Description
    - Extend `TurnContextItem` to include optional `base_instructions`,
    `user_instructions`, and `developer_instructions`.
    - Record the optional `final_output_json_schema` associated with a turn.
    - Add an optional `truncation_policy` to `TurnContextItem` and populate
    it when writing turn-context rollout items.
    - Introduce a protocol-level `TruncationPolicy` representation and
    convert from core truncation policy when recording.
    
    ### Testing
    - `cargo test -p codex-protocol` (pass)
  • feat: honor /etc/codex/config.toml (#8461)
    This adds logic to load `/etc/codex/config.toml` and associate it with
    `ConfigLayerSource::System` on UNIX. I refactored the code so it shares
    logic with the creation of the `ConfigLayerSource::User` layer.
  • feat: load ExecPolicyManager from ConfigLayerStack (#8453)
    https://github.com/openai/codex/pull/8354 added support for in-repo
    `.config/` files, so this PR updates the logic for loading `*.rules`
    files to load `*.rules` files from all relevant layers. The main change
    to the business logic is `load_exec_policy()` in
    `codex-rs/core/src/exec_policy.rs`.
    
    Note this adds a `config_folder()` method to `ConfigLayerSource` that
    returns `Option<AbsolutePathBuf>` so that it is straightforward to
    iterate over the sources and get the associated config folder, if any.
  • chore: include User layer in ConfigLayerStack even if config.toml is empty (#8456)
    This is necessary so that `$CODEX_HOME/skills` and `$CODEX_HOME/rules`
    still get loaded even if `$CODEX_HOME/config.toml` does not exist. See
    #8453.
    
    For now, it is possible to omit this layer when creating a dummy
    `ConfigLayerStack` in a test. We can revisit that later, if it turns out
    to be the right thing to do.
  • chore: add ConfigLayerStack as a field of Config (#8448)
    This should make it easier to load `.codex/skills/` and `.codex/rules/`
    properly throughout.
  • remove minimal client version (#8447)
    This isn't needed value by client
  • Handle concatenation nodes in bash command parser for exec policy (#8395)
    The bash command parser in exec_policy was failing to parse commands
    with concatenated flag-value patterns like `-g"*.py"` (no space between
    flag and quoted value). This caused policy rules like
    `prefix_rule(pattern=["rg"])` to not match commands such as `rg -n "foo"
    -g"*.py"`.
    
    When tree-sitter-bash parses `-g"*.py"`, it creates a "concatenation"
    node containing a word (`-g`) and a string (`"*.py"`). The parser
    previously rejected any node type not in the ALLOWED_KINDS list, causing
    the entire command parsing to fail and fall back to matching against the
    wrapped `bash -lc` command instead of the inner command.
    
    This change:
    - Adds "concatenation" to ALLOWED_KINDS in
    try_parse_word_only_commands_sequence
    - Adds handling for concatenation nodes in parse_plain_command_from_node
    that recursively extracts and joins word/string/raw_string children
    - Adds test cases for concatenated flag patterns with double and single
    quotes
    
    Fixes #8394
  • feat: add support for project_root_markers in config.toml (#8359)
    - allow configuring `project_root_markers` in `config.toml`
    (user/system/MDM) to control project discovery beyond `.git`
    - honor the markers after merging pre-project layers; default to
    `[".git"]` when unset and skip ancestor walk when set to an empty array
    - document the option and add coverage for alternate markers in config
    loader tests
  • feat: support in-repo .codex/config.toml entries as sources of config info (#8354)
    - We now support `.codex/config.toml` in repo (from `cwd` up to the
    first `.git` found, if any) as layers in `ConfigLayerStack`. A new
    `ConfigLayerSource::Project` variant was added to support this.
    - In doing this work, I realized that we were resolving relative paths
    in `config.toml` after merging everything into one `toml::Value`, which
    is wrong: paths should be relativized with respect to the folder
    containing the `config.toml` that was deserialized. This PR introduces a
    deserialize/re-serialize strategy to account for this in
    `resolve_config_paths()`. (This is why `Serialize` is added to so many
    types as part of this PR.)
    - Added tests to verify this new behavior.
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/8354).
    * #8359
    * __->__ #8354
  • Add ExecPolicyManager (#8349)
    Move exec policy management into services to keep turn context
    immutable.
  • feat(windows) start powershell in utf-8 mode (#7902)
    ## Summary
    Adds a FeatureFlag to enforce UTF8 encoding in powershell, particularly
    Windows Powershell v5. This should help address issues like #7290.
    
    Notably, this PR does not include the ability to parse `apply_patch`
    invocations within UTF8 shell commands (calls to the freeform tool
    should not be impacted). I am leaving this out of scope for now. We
    should address before this feature becomes Stable, but those cases are
    not the default behavior at this time so we're okay for experimentation
    phase. We should continue cleaning up the `apply_patch::invocation`
    logic and then can handle it more cleanly.
    
    ## Testing
    - [x] Adds additional testing
  • fix: do not panic on alphas (#8406)
    alphas are used sometimes as stable release
  • Remove plan from system skills (#8374)
    Removes plan from system skills. It has been rewritten into
    `create-plan` for evaluation and feedback:
    https://github.com/openai/skills/pull/22
  • Chore: remove rmcp feature and exp flag usages (#8087)
    ### Summary
    With codesigning on Mac, Windows and Linux, we should be able to safely
    remove `features.rmcp_client` and `use_experimental_use_rmcp_client`
    check from the codebase now.
  • feat(tui2): tune scrolling inpu based on (#8357)
    ## TUI2: Normalize Mouse Scroll Input Across Terminals (Wheel +
    Trackpad)
    
    This changes TUI2 scrolling to a stream-based model that normalizes
    terminal scroll event density into consistent wheel behavior (default:
    ~3 transcript lines per physical wheel notch) while keeping trackpad
    input higher fidelity via fractional accumulation.
    
    Primary code: `codex-rs/tui2/src/tui/scrolling/mouse.rs`
    
    Doc of record (model + probe-derived data):
    `codex-rs/tui2/docs/scroll_input_model.md`
    
    ### Why
    
    Terminals encode both mouse wheels and trackpads as discrete scroll
    up/down events with direction but no magnitude, and they vary widely in
    how many raw events they emit per physical wheel notch (commonly 1, 3,
    or 9+). Timing alone doesn’t reliably distinguish wheel vs trackpad, so
    cadence-based heuristics are unstable across terminals/hardware.
    
    This PR treats scroll input as short *streams* separated by silence or
    direction flips, normalizes raw event density into tick-equivalents,
    coalesces redraws for dense streams, and exposes explicit config
    overrides.
    
    ### What Changed
    
    #### Scroll Model (TUI2)
    
    - Stream detection
      - Start a stream on the first scroll event.
      - End a stream on an idle gap (`STREAM_GAP_MS`) or a direction flip.
    - Normalization
    - Convert raw events into tick-equivalents using per-terminal
    `tui.scroll_events_per_tick`.
    - Wheel-like vs trackpad-like behavior
    - Wheel-like: fixed “classic” lines per wheel notch; flush immediately
    for responsiveness.
    - Trackpad-like: fractional accumulation + carry across stream
    boundaries; coalesce flushes to ~60Hz to avoid floods and reduce “stop
    lag / overshoot”.
    - Trackpad divisor is intentionally capped: `min(scroll_events_per_tick,
    3)` so terminals with dense wheel ticks (e.g. 9 events per notch) don’t
    make trackpads feel artificially slow.
    - Auto mode (default)
      - Start conservatively as trackpad-like (avoid overshoot).
    - Promote to wheel-like if the first tick-worth of events arrives
    quickly.
    - Fallback for 1-event-per-tick terminals (no tick-completion timing
    signal).
    
    #### Trackpad Acceleration
    
    Some terminals produce relatively low vertical event density for
    trackpad gestures, which makes large/faster swipes feel sluggish even
    when small motions feel correct. To address that, trackpad-like streams
    apply a bounded multiplier based on event count:
    
    - `multiplier = clamp(1 + abs(events) / scroll_trackpad_accel_events,
    1..scroll_trackpad_accel_max)`
    
    The multiplier is applied to the trackpad stream’s computed line delta
    (including carried fractional remainder). Defaults are conservative and
    bounded.
    
    #### Config Knobs (TUI2)
    
    All keys live under `[tui]`:
    
    - `scroll_wheel_lines`: lines per physical wheel notch (default: 3).
    - `scroll_events_per_tick`: raw vertical scroll events per physical
    wheel notch (terminal-specific default; fallback: 3).
    - Wheel-like per-event contribution: `scroll_wheel_lines /
    scroll_events_per_tick`.
    - `scroll_trackpad_lines`: baseline trackpad sensitivity (default: 1).
    - Trackpad-like per-event contribution: `scroll_trackpad_lines /
    min(scroll_events_per_tick, 3)`.
    - `scroll_trackpad_accel_events` / `scroll_trackpad_accel_max`: bounded
    trackpad acceleration (defaults: 30 / 3).
    - `scroll_mode = auto|wheel|trackpad`: force behavior or use the
    heuristic (default: `auto`).
    - `scroll_wheel_tick_detect_max_ms`: auto-mode promotion threshold (ms).
    - `scroll_wheel_like_max_duration_ms`: auto-mode fallback for
    1-event-per-tick terminals (ms).
    - `scroll_invert`: invert scroll direction (applies to wheel +
    trackpad).
    
    Config docs: `docs/config.md` and field docs in
    `codex-rs/core/src/config/types.rs`.
    
    #### App Integration
    
    - The app schedules follow-up ticks to close idle streams (via
    `ScrollUpdate::next_tick_in` and `schedule_frame_in`) and finalizes
    streams on draw ticks.
      - `codex-rs/tui2/src/app.rs`
    
    #### Docs
    
    - Single doc of record describing the model + preserved probe
    findings/spec:
      - `codex-rs/tui2/docs/scroll_input_model.md`
    
    #### Other (jj-only friendliness)
    
    - `codex-rs/tui2/src/diff_render.rs`: prefer stable cwd-relative paths
    when the file is under the cwd even if there’s no `.git`.
    
    ### Terminal Defaults
    
    Per-terminal defaults are derived from scroll-probe logs (see doc).
    Notable:
    
    - Ghostty currently defaults to `scroll_events_per_tick = 3` even though
    logs measured ~9 in one setup. This is a deliberate stopgap; if your
    Ghostty build emits ~9 events per wheel notch, set:
    
      ```toml
      [tui]
      scroll_events_per_tick = 9
      ```
    
    ### Testing
    
    - `just fmt`
    - `just fix -p codex-core --allow-no-vcs`
    - `cargo test -p codex-core --lib` (pass)
    - `cargo test -p codex-tui2` (scroll tests pass; remaining failures are
    known flaky VT100 color tests in `insert_history`)
    
    ### Review Focus
    
    - Stream finalization + frame scheduling in `codex-rs/tui2/src/app.rs`.
    - Auto-mode promotion thresholds and the 1-event-per-tick fallback
    behavior.
    - Trackpad divisor cap (`min(events_per_tick, 3)`) and acceleration
    defaults.
    - Ghostty default tradeoff (3 vs ~9) and whether we should change it.