Commit Graph

785 Commits

  • Add public skills + improve repo skill discovery and error UX (#8098)
    1. Adds SkillScope::Public end-to-end (core + protocol) and loads skills
    from the public cache directory
    2. Improves repo skill discovery by searching upward for the nearest
    .codex/skills within a git repo
    3. Deduplicates skills by name with deterministic ordering to avoid
    duplicates across sources
    4. Fixes garbled “Skill errors” overlay rendering by preventing pending
    history lines from being injected during the modal
    5. Updates the project docs “Skills” intro wording to avoid hardcoded
    paths
  • feat: unified exec footer (#8067)
    <img width="452" height="205" alt="Screenshot 2025-12-15 at 17 54 44"
    src="https://github.com/user-attachments/assets/9ece0b1c-8387-4dfc-b883-c6a68ea1b663"
    />
  • refactor TUI event loop to enable dropping + recreating crossterm event stream (#7961)
    Introduces an `EventBroker` between the crossterm `EventStream` source
    and the consumers in the TUI. This enables dropping + recreating the
    `crossterm_events` without invalidating the consumer.
    
    Dropping and recreating the crossterm event stream enables us to fully
    relinquish `stdin` while the app keeps running. If the stream is not
    dropped, it will continue to read from `stdin` even when it is not
    actively being polled, potentially stealing input from other processes.
    See
    [here](https://www.reddit.com/r/rust/comments/1f3o33u/myterious_crossterm_input_after_running_vim/?utm_source=chatgpt.com)
    and [here](https://ratatui.rs/recipes/apps/spawn-vim/) for details.
    
    ### Tests
    Added tests for new `EventBroker` setup, existing tests pass, tested
    locally.
  • fix: Don't trigger keybindings view on input burst (#7980)
    Human TL;DR - in some situations, pasting/rapidly inputting text will
    currently cause `?` characters to be stripped from the input message
    content, and display the key bindings helper. For instance, writing
    "Where is X defined? Can we do Y?" results in "Where is X defined Can we
    do Y" being added to the message draft area. This is mildly annoying.
    
    The fix was a simple one line addition. Added a test, ran linters, and
    all looks good to me. I didn't create an issue to link to in this PR - I
    had submitted this bug as a report a while ago but can't seem to find it
    now. Let me know if it's an absolute must for the PR to be accepted.
    
    I have read the CLA Document and I hereby sign the CLA
    
    Below is Codex's summary.
    
    ---
    
    # `?` characters toggling shortcuts / being dropped
    
    ## Symptom
    
    On Termux (and potentially other terminal environments), composing text
    in the native input field and sending it to the TTY can cause:
    
    - The shortcuts overlay to appear (as if `?` was pressed on an empty
    prompt), and
    - All of the literal `?` characters in the text to be **missing** from
    the composer input,
      even when `?` is not the first character.
    
    This typically happens when the composer was previously empty and the
    terminal delivers the text as a rapid sequence of key events rather than
    a single bracketed paste event.
    
    ## Root cause
    
    The TUI has two relevant behaviors:
    
    1. **Shortcut toggle on `?` when empty**
    - `ChatComposer::handle_shortcut_overlay_key` treats a plain `?` press
    as a toggle between the shortcut summary and the full shortcut overlay,
    but only when the composer is empty.
    - When it toggles, it consumes the key event (so `?` is *not* inserted
    into the text input).
    
    2. **“Paste burst” buffering for fast key streams**
    - The TUI uses a heuristic to detect “paste-like” input bursts even when
    the terminal doesn’t send an explicit paste event.
    - During that burst detection, characters can be buffered (and the text
    area can remain empty temporarily) while the system decides whether to
    treat the stream as paste-like input.
    
    In Termux’s “send composed text all at once” mode, the input often
    arrives as a very fast stream of `KeyCode::Char(...)` events. While that
    stream is being buffered as a burst, the visible textarea can still be
    empty. If a `?` arrives during this window, it matches “empty composer”
    and is interpreted as “toggle shortcuts” instead of “insert literal
    `?`”, so the `?` is dropped.
    
    ## Fix
    
    Make the `?` toggle conditional on not being in any paste-burst
    transient state.
    
    Implementation:
    
    - `ChatComposer::handle_shortcut_overlay_key` now checks
    `!self.is_in_paste_burst()` in addition to `self.is_empty()` before
    toggling.
    - This ensures that when input is arriving as a fast burst (including
    the “pending first char” case), `?` is treated as normal text input
    rather than a UI toggle.
    
    ## Test coverage
    
    Added a test that simulates a Termux-like fast stream:
    
    - Sends `h i ? t h e r e` as immediate successive `KeyEvent::Char`
    events (no delays).
    - Asserts that a paste burst is active and the textarea is still empty
    while buffering.
    - Flushes the burst and verifies:
      - The final text contains the literal `?` (`"hi?there"`), and
      - The footer mode is not `ShortcutOverlay`.
    
    ## Notes
    
    This fix intentionally keeps the existing UX:
    
    - `?` still toggles shortcuts when the composer is genuinely empty and
    the user is not in the middle of entering text.
    - `?` typed while composing content (including IME/native-input fast
    streams) remains literal.
  • Reimplement skills loading using SkillsManager + skills/list op. (#7914)
    refactor the way we load and manage skills:
    1. Move skill discovery/caching into SkillsManager and reuse it across
    sessions.
    2. Add the skills/list API (Op::ListSkills/SkillsListResponse) to fetch
    skills for one or more cwds. Also update app-server for VSCE/App;
    3. Trigger skills/list during session startup so UIs preload skills and
    handle errors immediately.
  • Changed default wrap algorithm from OptimalFit to FirstFit (#7960)
    Codex identified this as the cause of a reported hang:
    https://github.com/openai/codex/issues/7822. Apparently, the wrapping
    algorithm we're using has known issues and bad worst-case behaviors when
    OptimalFit is used on certain strings. It recommended switching to
    FirstFit instead.
  • fix: introduce AbsolutePathBuf as part of sandbox config (#7856)
    Changes the `writable_roots` field of the `WorkspaceWrite` variant of
    the `SandboxPolicy` enum from `Vec<PathBuf>` to `Vec<AbsolutePathBuf>`.
    This is helpful because now callers can be sure the value is an absolute
    path rather than a relative one. (Though when using an absolute path in
    a Seatbelt config policy, we still have to _canonicalize_ it first.)
    
    Because `writable_roots` can be read from a config file, it is important
    that we are able to resolve relative paths properly using the parent
    folder of the config file as the base path.
  • fix: restore MCP startup progress messages in TUI (fixes #7827) (#7828)
    ## Problem
    
    The introduction of `notify_sandbox_state_change()` in #7112 caused a
    regression where the blocking call in `Session::new()` waits for all MCP
    servers to fully initialize before returning. This prevents the TUI
    event loop from starting, resulting in `McpStartupUpdateEvent` messages
    being emitted but never consumed or displayed. As a result, the app
    appears to hang during startup, and users do not see the expected
    "Booting MCP server: {name}" status line.
    
    Issue: [#7827](https://github.com/openai/codex/issues/7827)
    
    ## Solution
    This change moves sandbox state notification into each MCP server's
    background initialization task. The notification is sent immediately
    after the server transitions to the Ready state. This approach:
    - Avoids blocking `Session::new()`, allowing the TUI event loop to start
    promptly.
    - Ensures each MCP server receives its sandbox state before handling any
    tool calls.
    - Restores the display of "Booting MCP server" status lines during
    startup.
    
    ## Key Changes
    - Added `ManagedClient::notify_sandbox_state()` method.
    - Passed sandbox_state to `McpConnectionManager::initialize()`.
    - Sends sandbox state notification in the background task after the
    server reaches Ready status.
    - Removed blocking notify_sandbox_state_change() methods.
    - Added a chatwidget snapshot test for the "Booting MCP server" status
    line.
    
    ## Regression Details
    
    Regression was bisected to #7112, which introduced the blocking
    behavior.
    
    ---------
    
    Co-authored-by: Michael Bolin <bolinfest@gmail.com>
    Co-authored-by: Michael Bolin <mbolin@openai.com>
  • fix: break tui (#7876)
    Prevent TUI to loop for ever if one of the RX it's listing on get closed
  • fix(tui): show xhigh reasoning warning for gpt-5.2 (#7910)
    ## Notes
    - Extend reasoning-effort popup warning eligibility to gpt-5.2* models
    for the Extra High (xhigh) option.
    
    ## Revisions
    - R2: Remove unnecessary tests and snapshots
    - R1: initial
    
    ## Testing
    - `just fix`, `cargo test -p codex-tui`, and `cargo test -p codex-tui2`
    - Manual testing
    
    **Before**:
    <img width="864" height="162" alt="image"
    src="https://github.com/user-attachments/assets/d12a8f11-3ba5-4c31-9ae9-096a408b4971"
    />
    
    **After** (consistent with GPT 5.1 Codex Max):
    <img width="864" height="156" alt="image"
    src="https://github.com/user-attachments/assets/29c0ea7a-c68e-4fac-b10f-15a420ae5953"
    />
    
    <img width="684" height="154" alt="image"
    src="https://github.com/user-attachments/assets/b562b8b6-6e63-4dc2-8344-5c7f9a9b6263"
    />
  • Make migration screen dynamic (#7896)
    # External (non-OpenAI) Pull Request Requirements
    
    Before opening this Pull Request, please read the dedicated
    "Contributing" markdown file or your PR may be closed:
    https://github.com/openai/codex/blob/main/docs/contributing.md
    
    If your PR conforms to our contribution guidelines, replace this text
    with a detailed and high quality description of your changes.
    
    Include a link to a bug report or enhancement request.
  • Fix misleading 'maximize' high effort description on xhigh models (#7874)
    ## Notes
    - switch misleading High reasoning effort descriptions from "Maximizes
    reasoning depth" to "Higher reasoning depth" across models with xhigh
    reasoning. Affects GPT-5.1 Codex Max and Robin
    - refresh model list fixtures and chatwidget snapshots to match new copy
    
    ## Revision
    - R2: Change 'Higher' to 'Greater'
    - R1: Initial
    
    ## Testing
    
    <img width="583" height="142" alt="image"
    src="https://github.com/user-attachments/assets/1ddd8971-7841-4cb3-b9ba-91095a7435d2"
    />
    
    <img width="838" height="142" alt="image"
    src="https://github.com/user-attachments/assets/79aaedbf-7624-4695-b822-93dea7d6a800"
    />
  • Fix toasts on Windows under WSL 2 (#7137)
    Before this: no notifications or toasts when using Codex CLI in WSL 2.
    
    After this: I get toasts from Codex
  • fix: dont quit on 'q' in onboarding ApiKeyEntry state (#7869)
    ### What
    
    Don't treat `q` as a special quit character on the API key paste page in
    the onboarding flow.
    
    This addresses #7413, where pasting API keys with `q` would cause codex
    to quit on Windows.
    
    ### Test Plan
    
    Tested on Windows and MacOS.
  • feat: robin (#7882)
    <img width="554" height="554" alt="image"
    src="https://github.com/user-attachments/assets/aa86f4c8-fb34-4b0e-8b03-3a9980dfdb08"
    />
    
    ---------
    
    Co-authored-by: Dylan Hurd <dylan.hurd@openai.com>
  • fix(stuff) (#7855)
    Co-authored-by: Ahmed Ibrahim <aibrahim@openai.com>
  • Fixed regression that broke fuzzy matching for slash commands (#7859)
    This addresses bug #7857 which was introduced recently as part of PR
    #7704.
  • Inject SKILL.md when it's explicitly mentioned. (#7763)
    1. Skills load once in core at session start; the cached outcome is
    reused across core and surfaced to TUI via SessionConfigured.
    2. TUI detects explicit skill selections, and core injects the matching
    SKILL.md content into the turn when a selected skill is present.
  • fix: Prevent slash command popup from activating on invalid inputs (#7704)
    ## Slash Command popup issue
    
    #7659
    
    When recalling history, the
    composer(`codex_tui::bottom_pane::chat_composer`) restores the previous
    prompt text (which may start with `/`) and then calls
    `sync_command_popup`. The logic in `sync_command_popup` treats any first
    line that starts with `/` and has the caret inside the initial `/name`
    token as an active slash command name:
    
    ```rust
    let is_editing_slash_command_name = if first_line.starts_with('/') && caret_on_first_line {
        let token_end = first_line
            .char_indices()
            .find(|(_, c)| c.is_whitespace())
            .map(|(i, _)| i)
            .unwrap_or(first_line.len());
        cursor <= token_end
    } else {
        false
    };
    ```
    
    This detection does not distinguish between an actual interactive slash
    command being typed and a normal historical prompt that happens to begin
    with `/`. As a result, after history recall, the restored prompt like `/
    test` is interpreted as an "editing command name" context and the
    slash-command popup is (re)activated. Once `active_popup` is
    `ActivePopup::Command`, subsequent `Up` key presses are handled by
    `handle_key_event_with_slash_popup` instead of
    `handle_key_event_without_popup`, so they no longer trigger
    `history.navigate_up(...)` and the session prompt history cannot be
    scrolled.
  • make model optional in config (#7769)
    - Make Config.model optional and centralize default-selection logic in
    ModelsManager, including a default_model helper (with
    codex-auto-balanced when available) so sessions now carry an explicit
    chosen model separate from the base config.
    - Resolve `model` once in `core` and `tui` from config. Then store the
    state of it on other structs.
    - Move refreshing models to be before resolving the default model
  • Removed experimental "command risk assessment" feature (#7799)
    This experimental feature received lukewarm reception during internal
    testing. Removing from the code base.
  • Express rate limit warning as % remaining (#7795)
    <img width="342" height="264" alt="image"
    src="https://github.com/user-attachments/assets/f1e932ff-c550-47b3-9035-0299ada4998d"
    />
    
    Earlier, the warning was expressed as consumed% whereas status was
    expressed as remaining%. This change brings the two into sync to
    minimize confusion and improve visual consistency.
  • Add vim-style navigation for CLI option selection (#7784)
    ## Summary
    
    Support "j" and "k" keys as aliases for "down" and "up" so vim users
    feel loved. Only support these keys when the selection is not
    searchable.
    
    ## Testing
    - env -u NO_COLOR TERM=xterm-256color cargo test -p codex-tui
    
    
    ------
    [Codex
    Task](https://chatgpt.com/codex/tasks/task_i_693771b53bc8833088669060dfac2083)
  • Add vim navigation keys to transcript pager (#7550)
    ## Summary
    - add vim-style pager navigation for transcript overlays (j/k,
    ctrl+f/b/d/u) without removing existing keys
    - add shift-space to page up
    
    ------
    [Codex
    Task](https://chatgpt.com/codex/tasks/task_i_69309d26da508329908b2dc8ca40afb7)
  • Fix transcript pager page continuity (#7363)
    ## What
    
    Fix PageUp/PageDown behaviour in the Ctrl+T transcript overlay so that
    paging is continuous and reversible, and add tests to lock in the
    expected behaviour.
    
    ## Why
    
    Today, paging in the transcript overlay uses the raw viewport height
    instead of the effective content height after layout. Because the
    overlay reserves some rows for chrome (header/footer), this can cause:
    
    - PageDown to skip transcript lines between pages.
    - PageUp/PageDown not to “round-trip” cleanly (PageDown then PageUp does
    not always return to the same set of visible lines).
    
    This shows up when inspecting longer transcripts via Ctrl+T; see #7356
    for context.
    
    ## How
    
    - Add a dedicated `PagerView::page_step` helper that computes the page
    size from the last rendered content height and falls back to
    `content_area(viewport_area).height` when that is not yet available.
    - Use `page_step(...)` for both PageUp and PageDown (including SPACE) so
    the scroll step always matches the actual content area height, not the
    full viewport height.
    - Add a focused test
    `transcript_overlay_paging_is_continuous_and_round_trips` that:
      - Renders a synthetic transcript with numbered `line-NN` rows.
    - Asserts that successive PageDown operations show continuous line
    numbers (no gaps).
    - Asserts that PageDown+PageUp and PageUp+PageDown round-trip correctly
    from non-edge offsets.
    
    The change is limited to `codex-rs/tui/src/pager_overlay.rs` and only
    affects the transcript overlay paging semantics.
    
    ## Related issue
    
    - #7356
    
    ## Testing
    
    On Windows 11, using PowerShell 7 in the repo root:
    
    ```powershell
    cargo test
    cargo clippy --tests
    cargo fmt -- --config imports_granularity=Item
    ```
    
    - All tests passed.
    - `cargo clippy --tests` reported some pre-existing warnings that are
    unrelated to this change; no new lints were introduced in the modified
    code.
    
    ---------
    
    Signed-off-by: muyuanjin <24222808+muyuanjin@users.noreply.github.com>
    Co-authored-by: Eric Traut <etraut@openai.com>
  • Remove legacy ModelInfo and merge it with ModelFamily (#7748)
    This is a step towards removing the need to know `model` when
    constructing config. We firstly don't need to know `model_info` and just
    respect if the user has already set it. Next step, we don't need to know
    `model` unless the user explicitly set it in `config.toml`
  • Restore status header after stream recovery (#7660)
    ## Summary
    - restore the previous status header when a non-error event arrives
    after a stream retry
    - add a regression test to ensure the reconnect banner clears once
    streaming resumes
    
    ## Testing
    - cargo fmt -- --config imports_granularity=Item
    - cargo clippy --fix --all-features --tests --allow-dirty -p codex-tui
    - NO_COLOR=0 cargo test -p codex-tui *(fails: vt100 color assertion
    tests expect colored cells but the environment returns Default colors
    even with NO_COLOR cleared and TERM/COLORTERM set)*
    
    ------
    [Codex
    Task](https://chatgpt.com/codex/tasks/task_i_69337f8c77508329b3ea85134d4a7ac7)
  • Enhance model picker (#7709)
    # External (non-OpenAI) Pull Request Requirements
    
    Before opening this Pull Request, please read the dedicated
    "Contributing" markdown file or your PR may be closed:
    https://github.com/openai/codex/blob/main/docs/contributing.md
    
    If your PR conforms to our contribution guidelines, replace this text
    with a detailed and high quality description of your changes.
    
    Include a link to a bug report or enhancement request.
  • load models from disk and set a ttl and etag (#7722)
    # External (non-OpenAI) Pull Request Requirements
    
    Before opening this Pull Request, please read the dedicated
    "Contributing" markdown file or your PR may be closed:
    https://github.com/openai/codex/blob/main/docs/contributing.md
    
    If your PR conforms to our contribution guidelines, replace this text
    with a detailed and high quality description of your changes.
    
    Include a link to a bug report or enhancement request.
  • fix(tui): add missing Ctrl+n/Ctrl+p support to ListSelectionView (#7629)
    ## Summary
    
    Extend Ctrl+n/Ctrl+p navigation support to selection popups (model
    picker, approval mode, etc.)
    
    This is a follow-up to #7530, which added Ctrl+n/Ctrl+p navigation to
    the textarea.
    The same keybindings were missing from `ListSelectionView`, causing
    inconsistent behavior
      when navigating selection popups.
    
      ## Related
    
      - #7530 - feat(tui): map Ctrl-P/N to arrow navigation in textarea
    
      ## Changes
    
      - Added Ctrl+n as alternative to Down arrow in selection popups
      - Added Ctrl+p as alternative to Up arrow in selection popups
      - Added unit tests for the new keybindings
    
      ## Test Plan
    
      - [x] `cargo test -p codex-tui list_selection_view` - all tests pass
    - [x] Manual testing: verified Ctrl+n/p navigation works in model
    selection popup
    
    ---------
    
    Co-authored-by: Eric Traut <etraut@openai.com>
  • fix wrap behavior for long commands (#7655)
    before:
    <img width="720" height="225" alt="image"
    src="https://github.com/user-attachments/assets/19b7ad7c-db14-4792-97cc-80677a3a52ec"
    />
    after:
    <img width="500" height="219" alt="Screenshot 2025-12-05 at 4 37 14 PM"
    src="https://github.com/user-attachments/assets/f877f846-5943-4ca7-8949-89e8524ffdb9"
    />
    
    
    also removes `is_current`, which is deadcode
  • fix: update URLs to use HTTPS in model migration prompts (#7705)
    Update URLs to use HTTPS in model migration prompts
    
    Closes #6685
  • Wire with_remote_overrides to construct model families (#7621)
    - This PR wires `with_remote_overrides` and make the
    `construct_model_families` an async function
    - Moves getting model family a level above to keep the function `sync`
    - Updates the tests to local, offline, and `sync` helper for model
    families
  • fix: taking plan type from usage endpoint instead of thru auth token (#7610)
    pull plan type from the usage endpoint, persist it in session state /
    tui state, and propagate through rate limit snapshots
  • Call models endpoint in models manager (#7616)
    - Introduce `with_remote_overrides` and update
    `refresh_available_models`
    - Put `auth_manager` instead of `auth_mode` on `models_manager`
    - Remove `ShellType` and `ReasoningLevel` to use already existing
    structs
  • Fix handle_shortcut_overlay_key for cross-platform consistency (#7583)
    **Summary**
    - Shortcut toggle using `?` in `handle_shortcut_overlay_key` fails to
    trigger on some platforms (notably Windows). Current match requires
    `KeyCode::Char('?')` with `KeyModifiers::NONE`. Some terminals set
    `SHIFT` when producing `?` (since it is typically `Shift + /`), so the
    strict `NONE` check prevents toggling.
    
    **Impact**
    - On Windows consoles/terminals, pressing `?` with an empty composer
    often does nothing, leading to inconsistent UX compared to macOS/Linux.
    
    **Root Cause**
    - Crossterm/terminal backends report modifiers inconsistently across
    platforms. Generating `?` may include `SHIFT`. The code enforces
    `modifiers == NONE`, so valid `?` presses with `SHIFT` are ignored.
    AltGr keyboards may also surface as `ALT`.
    
    **Repro Steps**
    - Open the TUI, ensure the composer is empty.
    - Press `?`.
    - Expected: Shortcut overlay toggles.
    - Actual (Windows frequently): No toggle occurs.
    
    **Fix Options**
    - Option 1 (preferred): Accept `?` regardless of `SHIFT`, but reject
    `CONTROL` and `ALT`.
    - Rationale: Keeps behavior consistent across platforms with minimal
    code change.
    	- Example change:
    		- Before: matching `KeyModifiers::NONE` only.
    		- After: allow `SHIFT`, disallow `CONTROL | ALT`.
    		- Suggested condition:
    			```rust
    			let toggles = matches!(key_event.code, KeyCode::Char('?'))
    && !key_event.modifiers.intersects(KeyModifiers::CONTROL |
    KeyModifiers::ALT)
    					&& self.is_empty();
    			```
    
    - Option 2: Platform-specific handling (Windows vs non-Windows).
    - Implement two variants or conditional branches using `#[cfg(target_os
    = "windows")]`.
    - On Windows, accept `?` with `SHIFT`; on other platforms, retain
    current behavior.
    - Trade-off: Higher maintenance burden and code divergence for limited
    benefit.
    
    ---
    
    close #5495
  • remove model_family from `config (#7571)
    - Remove `model_family` from `config`
    - Make sure to still override config elements related to `model_family`
    like supporting reasoning
  • FIX: WSL Paste image does not work (#6793)
    ## Related issues:  
    - https://github.com/openai/codex/issues/3939  
    - https://github.com/openai/codex/issues/2292  
    - https://github.com/openai/codex/issues/7528 (After correction
    https://github.com/openai/codex/pull/3990)
    
    **Area:** `codex-cli` (image handling / clipboard & file uploads)  
    **Platforms affected:** WSL (Ubuntu on Windows 10/11). No behavior
    change on native Linux/macOS/Windows.
    
    ## Summary
    
    This PR fixes image pasting and file uploads when running `codex-cli`
    inside WSL. Previously, image operations failed silently or with
    permission errors because paths weren't properly mapped between Windows
    and WSL filesystems.
    
    ## Visual Result
    
    <img width="1118" height="798" alt="image"
    src="https://github.com/user-attachments/assets/14e10bc4-6b71-4d1f-b2a6-52c0a67dd069"
    />
    
    ## Last Rust-Cli
    
    <img width="1175" height="859" alt="image"
    src="https://github.com/user-attachments/assets/7ef41e29-9118-42c9-903c-7116d21e1751"
    />
    
    ## Root cause
    
    The CLI assumed native Linux/Windows environments and didn't handle the
    WSL↔Windows boundary:
    
    - Used Linux paths for files that lived on the Windows host
    - Missing path normalization between Windows (`C:\...`) and WSL
    (`/mnt/c/...`)
    - Clipboard access failed under WSL
    
    ### Why `Ctrl+V` doesn't work in WSL terminals
    
    Most WSL terminal emulators (Windows Terminal, ConEmu, etc.) intercept
    `Ctrl+V` at the terminal level to paste text from the Windows clipboard.
    This keypress never reaches the CLI application itself, so our clipboard
    image handler never gets triggered. Users need `Ctrl+Alt+V`.
    
    ## Changes
    
    ### WSL detection & path mapping
    
    - Detects WSL by checking `/proc/sys/kernel/osrelease` and the
    `WSL_INTEROP` env var
    - Maps Windows drive paths to WSL mount paths (`C:\...` → `/mnt/c/...`)
    
    ### Clipboard fallback for WSL
    
    - When clipboard access fails under WSL, falls back to PowerShell to
    extract images from the Windows clipboard
    - Saves to a temp file and maps the path back to WSL
    
    ### UI improvements
    
    - Shows `Ctrl+Alt+V` hint on WSL (many terminals intercept plain
    `Ctrl+V`)
    - Better error messages for unreadable images
    
    ## Performance
    
    - Negligible overhead. The fallback adds a single FS copy to a temp file
    only when needed.
    - Direct streaming remains the default.
    
    ## Files changed
    
    - `protocol/src/lib.rs` – Added platform detection module  
    - `protocol/src/models.rs` – Added WSL path mapping for local images  
    - `protocol/src/platform.rs` – New module with WSL detection utilities  
    - `tui/src/bottom_pane/chat_composer.rs` – Added base64 data URL support
    and WSL path mapping
    - `tui/src/bottom_pane/footer.rs` – WSL-aware keyboard shortcuts  
    - `tui/src/clipboard_paste.rs` – PowerShell clipboard fallback
    
    ## How to reproduce the original bug (pre-fix)
    
    1. Run `codex-cli` inside WSL2 on Windows.  
    2. Paste an image from the Windows clipboard or drag an image from
    `C:\...` into the terminal.
    3. Observe that the image is not attached (silent failure) or an error
    is logged; no artifact reaches the tool.
    
    ## How to verify the fix
    
    1. Build this branch and run `codex-cli` inside WSL2.  
    2. Paste from clipboard and drag from both Windows and WSL paths.  
    3. Confirm that the image appears in the tool and the CLI shows a single
    concise info line (no warning unless fallback was used).
    
    I’m happy to adjust paths, naming, or split helpers into a separate
    module if you prefer.
    
    ## How to try this branch
    
    If you want to try this before it’s merged, you can use my Git branch:
    
    Repository: https://github.com/Waxime64/codex.git  
    Branch: `wsl-image-2`
    
    1. Start WSL on your Windows machine.
    2. Clone the repository and switch to the branch:
       ```bash
       git clone https://github.com/Waxime64/codex.git
       cd codex
       git checkout wsl-image-2
       # then go into the Rust workspace root, e.g.:
       cd codex-rs
    3. Build the TUI binary:
      cargo build -p codex-tui --bin codex-tui --release
    4. Install the binary:
       sudo install -m 0755 target/release/codex-tui /usr/local/bin/codex
    5. From the project directory where you want to use Codex, start it
    with:
       cd /path/to/your/project
       /usr/local/bin/codex
    
    On WSL, use CTRL+ALT+V to paste an image from the Windows clipboard into
    the chat.