Commit Graph

45 Commits

  • feat(tui): add configurable keymap support (#18593)
    ## Why
    
    The TUI currently handles keyboard shortcuts as hard-coded event matches
    spread across app, composer, pager, list, approval, and navigation code.
    That makes shortcuts hard to customize, makes displayed hints easy to
    drift from actual behavior, and makes future keymap work riskier because
    there is no central action inventory.
    
    This PR adds the foundation for configurable, action-based keymaps
    without adding the interactive remapping UI yet. Onboarding
    intentionally stays on fixed startup shortcuts because users cannot
    reasonably configure keymaps before completing onboarding.
    
    This is PR1 in the keymap stack:
    
    - PR1: #18593: configurable keymap foundation
    - PR2: #18594: `/keymap` picker and guided remapping UI
    - PR3: #18595: Vim composer mode and the remap option
    
    ## Design Notes
    
    The new model resolves named actions into concrete runtime bindings once
    from config, then passes those bindings to the UI surfaces that handle
    input or render shortcut hints.
    
    The main concepts are:
    
    - **Context**: a scope where an action is active, such as `global`,
    `chat`, `composer`, `editor`, `pager`, `list`, or `approval`.
    - **Action**: a named operation inside a context, such as
    `global.open_transcript`, `composer.submit`, or `pager.close`.
    - **Binding**: one or more single-key shortcuts assigned to an action,
    written as config strings such as `ctrl-t`, `alt-backspace`, or
    `page-down`. Multi-step sequences such as `ctrl-x ctrl-s`, `g g`, or
    leader-key flows are not part of this PR.
    - **Resolution order**: context-specific config wins first, supported
    global fallbacks come next, and built-in defaults fill in anything
    unset.
    - **Explicit unbinding**: an empty array removes an action binding in
    that scope and does not fall through to a fallback binding.
    - **Conflict validation**: a resolved keymap rejects duplicate active
    bindings inside the same scope so one keypress cannot dispatch two
    actions.
    
    ## What Changed
    
    - Added `TuiKeymap` config support under `[tui.keymap]`, including typed
    contexts/actions, key alias normalization, generated schema coverage,
    and user-facing config errors.
    - Added `RuntimeKeymap` resolution in `codex-rs/tui/src/keymap.rs`,
    including fallback precedence, built-in defaults, explicit unbinding,
    and per-context conflict validation.
    - Rewired existing TUI handlers to consume resolved keymap actions
    instead of directly matching hard-coded keys in each component.
    - Updated key hint rendering and footer/pager/list surfaces so displayed
    shortcuts follow the resolved keymap.
    - Kept onboarding shortcuts fixed in
    `codex-rs/tui/src/onboarding/keys.rs` instead of exposing them through
    `[tui.keymap]`.
    
    ## Validation
    
    The branch includes focused coverage for config parsing, key
    normalization, runtime fallback resolution, explicit unbinding,
    duplicate-key conflict validation, default keymap consistency,
    onboarding startup key behavior, and UI hint snapshots affected by
    resolved key bindings.
  • Hide rewind preview when no user message exists (#19510)
    ## Why
    
    Fixes #19508.
    
    In a fresh TUI session, pressing `Esc` twice entered the rewind
    transcript overlay even though there was no user message to rewind to.
    That produced an empty header-only transcript view and exposed a rewind
    flow that could not select a valid target.
    
    ## What changed
    
    The backtrack flow now checks whether a user-message rewind target
    exists before opening the transcript preview. If no target exists, Codex
    stays in the main TUI and shows `No previous message to edit.` instead
    of opening an empty overlay.
    
    The same guard applies when starting rewind preview from the transcript
    overlay, and the first `Esc` no longer advertises the “edit previous
    message” hint when there is no previous message available.
    
    Snapshot coverage was added for the unavailable rewind info message,
    along with a small target-detection test.
  • 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
  • fix(tui): keep /copy aligned with rollback (#18739)
    ## Why
    
    Fixes #18718.
    
    After rewinding a thread, `/copy` could still copy the latest assistant
    response from before the rewind. The transcript cells were rolled back,
    but the copy source was a single `last_agent_markdown` cache that was
    not synchronized with backtracking, so the visible conversation and
    copied content could diverge.
    
    ## What changed
    
    `ChatWidget` now keeps a bounded copy history for the most recent 32
    assistant responses, keyed by the visible user-turn count. When local
    rollback trims transcript cells, the copy cache is trimmed to the same
    surviving user-turn count so `/copy` uses the latest visible assistant
    response.
    
    If the user rewinds past the retained copy window, `/copy` now reports:
    
    ```text
    Cannot copy that response after rewinding. Only the most recent 32 responses are available to /copy.
    ```
    
    The change also adds coverage for copying the latest surviving response
    after rollback and for the over-limit rewind message.
    
    ## Verification
    
    - Manually resumed a synthetic 35-turn session, rewound within the
    retained window, and verified `/copy` copied the surviving response.
    - Manually rewound past the retained window and verified `/copy` showed
    the 32-response limit message.
    - `cargo test -p codex-tui slash_copy`
    - `just fix -p codex-tui`
    - `cargo insta pending-snapshots`
    
    Note: `cargo test -p codex-tui` currently fails on unrelated model
    catalog and snapshot drift around the default model changing to
    `gpt-5.4`; the focused `/copy` tests pass after fixing the new test
    setup.
  • 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>
  • Rename tui_app_server to tui (#16104)
    This is a follow-up to https://github.com/openai/codex/pull/15922. That
    previous PR deleted the old `tui` directory and left the new
    `tui_app_server` directory in place. This PR renames `tui_app_server` to
    `tui` and fixes up all references.
  • Remove the legacy TUI split (#15922)
    This is the part 1 of 2 PRs that will delete the `tui` /
    `tui_app_server` split. This part simply deletes the existing `tui`
    directory and marks the `tui_app_server` feature flag as removed. I left
    the `tui_app_server` feature flag in place for now so its presence
    doesn't result in an error. It is simply ignored.
    
    Part 2 will rename the `tui_app_server` directory `tui`. I did this as
    two parts to reduce visible code churn.
  • Apply argument comment lint across codex-rs (#14652)
    ## Why
    
    Once the repo-local lint exists, `codex-rs` needs to follow the
    checked-in convention and CI needs to keep it from drifting. This commit
    applies the fallback `/*param*/` style consistently across existing
    positional literal call sites without changing those APIs.
    
    The longer-term preference is still to avoid APIs that require comments
    by choosing clearer parameter types and call shapes. This PR is
    intentionally the mechanical follow-through for the places where the
    existing signatures stay in place.
    
    After rebasing onto newer `main`, the rollout also had to cover newly
    introduced `tui_app_server` call sites. That made it clear the first cut
    of the CI job was too expensive for the common path: it was spending
    almost as much time installing `cargo-dylint` and re-testing the lint
    crate as a representative test job spends running product tests. The CI
    update keeps the full workspace enforcement but trims that extra
    overhead from ordinary `codex-rs` PRs.
    
    ## What changed
    
    - keep a dedicated `argument_comment_lint` job in `rust-ci`
    - mechanically annotate remaining opaque positional literals across
    `codex-rs` with exact `/*param*/` comments, including the rebased
    `tui_app_server` call sites that now fall under the lint
    - keep the checked-in style aligned with the lint policy by using
    `/*param*/` and leaving string and char literals uncommented
    - cache `cargo-dylint`, `dylint-link`, and the relevant Cargo
    registry/git metadata in the lint job
    - split changed-path detection so the lint crate's own `cargo test` step
    runs only when `tools/argument-comment-lint/*` or `rust-ci.yml` changes
    - continue to run the repo wrapper over the `codex-rs` workspace, so
    product-code enforcement is unchanged
    
    Most of the code changes in this commit are intentionally mechanical
    comment rewrites or insertions driven by the lint itself.
    
    ## Verification
    
    - `./tools/argument-comment-lint/run.sh --workspace`
    - `cargo test -p codex-tui-app-server -p codex-tui`
    - parsed `.github/workflows/rust-ci.yml` locally with PyYAML
    
    ---
    
    * -> #14652
    * #14651
  • chore: remove codex-core public protocol/shell re-exports (#12432)
    ## Why
    
    `codex-rs/core/src/lib.rs` re-exported a broad set of types and modules
    from `codex-protocol` and `codex-shell-command`. That made it easy for
    workspace crates to import those APIs through `codex-core`, which in
    turn hides dependency edges and makes it harder to reduce compile-time
    coupling over time.
    
    This change removes those public re-exports so call sites must import
    from the source crates directly. Even when a crate still depends on
    `codex-core` today, this makes dependency boundaries explicit and
    unblocks future work to drop `codex-core` dependencies where possible.
    
    ## What Changed
    
    - Removed public re-exports from `codex-rs/core/src/lib.rs` for:
    - `codex_protocol::protocol` and related protocol/model types (including
    `InitialHistory`)
      - `codex_protocol::config_types` (`protocol_config_types`)
    - `codex_shell_command::{bash, is_dangerous_command, is_safe_command,
    parse_command, powershell}`
    - Migrated workspace Rust call sites to import directly from:
      - `codex_protocol::protocol`
      - `codex_protocol::config_types`
      - `codex_protocol::models`
      - `codex_shell_command`
    - Added explicit `Cargo.toml` dependencies (`codex-protocol` /
    `codex-shell-command`) in crates that now import those crates directly.
    - Kept `codex-core` internal modules compiling by using `pub(crate)`
    aliases in `core/src/lib.rs` (internal-only, not part of the public
    API).
    - Updated the two utility crates that can already drop a `codex-core`
    dependency edge entirely:
      - `codex-utils-approval-presets`
      - `codex-utils-cli`
    
    ## Verification
    
    - `cargo test -p codex-utils-approval-presets`
    - `cargo test -p codex-utils-cli`
    - `cargo check --workspace --all-targets`
    - `just clippy`
  • tui: preserve remote image attachments across resume/backtrack (#10590)
    ## Summary
    This PR makes app-server-provided image URLs first-class attachments in
    TUI, so they survive resume/backtrack/history recall and are resubmitted
    correctly.
    
    <img width="715" height="491" alt="Screenshot 2026-02-12 at 8 27 08 PM"
    src="https://github.com/user-attachments/assets/226cbd35-8f0c-4e51-a13e-459ef5dd1927"
    />
    
    Can delete the attached image upon backtracking:
    <img width="716" height="301" alt="Screenshot 2026-02-12 at 8 27 31 PM"
    src="https://github.com/user-attachments/assets/4558d230-f1bd-4eed-a093-8e1ab9c6db27"
    />
    
    In both history and composer, remote images are rendered as normal
    `[Image #N]` placeholders, with numbering unified with local images.
    
    ## What changed
    - Plumb remote image URLs through TUI message state:
      - `UserHistoryCell`
      - `BacktrackSelection`
      - `ChatComposerHistory::HistoryEntry`
      - `ChatWidget::UserMessage`
    - Show remote images as placeholder rows inside the composer box (above
    textarea), and in history cells.
    - Support keyboard selection/deletion for remote image rows in composer
    (`Up`/`Down`, `Delete`/`Backspace`).
    - Preserve remote-image-only turns in local composer history (Up/Down
    recall), including restore after backtrack.
    - Ensure submit/queue/backtrack resubmit include remote images in model
    input (`UserInput::Image`), and keep request shape stable for
    remote-image-only turns.
    - Keep image numbering contiguous across remote + local images:
      - remote images occupy `[Image #1]..[Image #M]`
      - local images start at `[Image #M+1]`
      - deletion renumbers consistently.
    - In protocol conversion, increment shared image index for remote images
    too, so mixed remote/local image tags stay in a single sequence.
    - Simplify restore logic to trust in-memory attachment order (no
    placeholder-number parsing path).
    - Backtrack/replay rollback handling now queues trims through
    `AppEvent::ApplyThreadRollback` and syncs transcript overlay/deferred
    lines after trims, so overlay/transcript state stays consistent.
    - Trim trailing blank rendered lines from user history rendering to
    avoid oversized blank padding.
    
    ## Docs + tests
    - Updated: `docs/tui-chat-composer.md` (remote image flow,
    selection/deletion, numbering offsets)
    - Added/updated tests across `tui/src/chatwidget/tests.rs`,
    `tui/src/app.rs`, `tui/src/app_backtrack.rs`, `tui/src/history_cell.rs`,
    and `tui/src/bottom_pane/chat_composer.rs`
    - Added snapshot coverage for remote image composer states, including
    deleting the first of two remote images.
    
    ## Validation
    - `just fmt`
    - `cargo test -p codex-tui`
    
    ## Codex author
    `codex fork 019c2636-1571-74a1-8471-15a3b1c3f49d`
  • tui: queue non-pending rollback trims in app-event order (#11373)
    ## Summary
    
    This PR fixes TUI transcript-sync behavior for
    `EventMsg::ThreadRolledBack` and makes rollback application order
    deterministic.
    
    Previously, rollback handling depended on `pending_rollback`:
    
    - if `pending_rollback` was set (local backtrack), TUI trimmed correctly
    - otherwise, replayed/external rollbacks were either ignored or could be
    applied at the wrong time relative to queued transcript inserts
    
    This change keeps the local backtrack path intact and routes non-pending
    rollbacks through the app event queue so rollback trims are applied in
    FIFO order with transcript cell inserts.
    
    ## What changed
    
    - Added/used `trim_transcript_cells_drop_last_n_user_turns(...)` for
    rollback-by-`num_turns` semantics.
    - Renamed rollback app event:
    - `AppEvent::ApplyReplayedThreadRollback` ->
    `AppEvent::ApplyThreadRollback`
    - Replay path (`ChatWidget`) now emits `ApplyThreadRollback`.
    - Live non-pending rollback path (`App::handle_backtrack_event`) now
    emits `ApplyThreadRollback` instead of trimming immediately.
    - App-level event handler applies `ApplyThreadRollback` after queued
    `InsertHistoryCell` events and schedules redraw only when a trim
    occurred.
    - When a trim occurs with an overlay open, TUI now syncs transcript
    overlay committed cells, clamps backtrack preview selection, and clears
    stale `deferred_history_lines` so closed overlays do not re-append
    rolled-back lines.
    - Clarified inline comments around the `pending_rollback` branch so
    future readers can reason about why there are two paths.
    
    ## Why queueing matters
    
    During resume/replay, transcript cells are populated via queued
    `InsertHistoryCell` app events. If a rollback is applied immediately
    outside that queue, it can run against an incomplete transcript and
    under-trim. Queueing non-pending rollbacks ensures consistent ordering
    and correct final transcript state.
    
    ## Behavior by rollback source
    
    - `pending_rollback = Some(...)` (local backtrack requested by this
    TUI):
      - use `finish_pending_backtrack()` and the stored selection boundary
    - `pending_rollback = None` (replay/external/non-local rollback):
    - enqueue `AppEvent::ApplyThreadRollback { num_turns }` and trim in
    app-event order
    
    ## Tests
    
    Added/updated tests covering ordering and semantics:
    
    -
    `app_backtrack::tests::trim_drop_last_n_user_turns_applies_rollback_semantics`
    - `app_backtrack::tests::trim_drop_last_n_user_turns_allows_overflow`
    - `app::tests::replayed_initial_messages_apply_rollback_in_queue_order`
    -
    `app::tests::live_rollback_during_replay_is_applied_in_app_event_order`
    -
    `app::tests::queued_rollback_syncs_overlay_and_clears_deferred_history`
    - `chatwidget::tests::replayed_thread_rollback_emits_ordered_app_event`
    
    Validation run:
    
    - `just fmt`
    - `cargo test -p codex-tui`
  • Persist text element ranges and attached images across history/resume (#9116)
    **Summary**
    - Backtrack selection now rehydrates `text_elements` and
    `local_image_paths` from the chosen user history cell so Esc‑Esc history
    edits preserve image placeholders and attachments.
    - Composer prefill uses the preserved elements/attachments in both `tui`
    and `tui2`.
    - Extended backtrack selection tests to cover image placeholder elements
    and local image paths.
    
    **Changes**
    - `tui/src/app_backtrack.rs`: Backtrack selection now carries text
    elements + local image paths; composer prefill uses them (removes TODO).
    - `tui2/src/app_backtrack.rs`: Same as above.
    - `tui/src/app.rs`: Updated backtrack test to assert restored
    elements/paths.
    - `tui2/src/app.rs`: Same test updates.
    
    ### The original scope of this PR (threading text elements and image
    attachments through the codex harness thoroughly/persistently) was
    broken into the following PRs other than this one:
    
    The diff of this PR was reduced by changing types in a starter PR:
    https://github.com/openai/codex/pull/9235
    
    Then text element metadata was added to protocol, app server, and core
    in this PR: https://github.com/openai/codex/pull/9331
    
    Then the end-to-end flow was completed by wiring TUI/TUI2 input,
    history, and restore behavior in
    https://github.com/openai/codex/pull/9393
    
    Prompt expansion was supported in this PR:
    https://github.com/openai/codex/pull/9518
    
    TextElement optional placeholder field was protected in
    https://github.com/openai/codex/pull/9545
  • Persist text elements through TUI input and history (#9393)
    Continuation of breaking up this PR
    https://github.com/openai/codex/pull/9116
    
    ## Summary
    - Thread user text element ranges through TUI/TUI2 input, submission,
    queueing, and history so placeholders survive resume/edit flows.
    - Preserve local image attachments alongside text elements and rehydrate
    placeholders when restoring drafts.
    - Keep model-facing content shapes clean by attaching UI metadata only
    to user input/events (no API content changes).
    
    ## Key Changes
    - TUI/TUI2 composer now captures text element ranges, trims them with
    text edits, and restores them when submission is suppressed.
    - User history cells render styled spans for text elements and keep
    local image paths for future rehydration.
    - Initial chat widget bootstraps accept empty `initial_text_elements` to
    keep initialization uniform.
    - Protocol/core helpers updated to tolerate the new InputText field
    shape without changing payloads sent to the API.
  • tui: allow forward navigation in backtrack preview (#9059)
    Fixes #9058
    
    ## Summary
    When the transcript backtrack preview is armed (press `Esc`), allow
    navigating to newer user messages with the `→` arrow, in addition to
    navigating backwards with `Esc`/`←`, before confirming with `Enter`.
    
    ## Changes
    - Backtrack preview navigation: `Esc`/`←` steps to older user messages,
    `→` steps to newer ones, `Enter` edits the selected message (clamped at
    bounds, no wrap-around).
    - Transcript overlay footer hints updated to advertise `esc/←`, `→`, and
    `enter` when a message is highlighted.
    
    ## Related
    - WSL shortcut-overlay snapshot determinism: #9359
    
    ## Testing
    - `just fmt`
    - `just fix -p codex-tui`
    - `just fix -p codex-tui2`
    - `cargo test -p codex-tui app_backtrack::`
    - `cargo test -p codex-tui pager_overlay::`
    - `cargo test -p codex-tui2 app_backtrack::`
    - `cargo test -p codex-tui2 pager_overlay::`
    
    ---------
    
    Co-authored-by: Josh McKinney <joshka@openai.com>
  • fix(tui) Defer backtrack trim until rollback confirms (#9401)
    Document the backtrack/rollback state machine and invariants between the
    transcript overlay, in-flight “live tail”, and core thread state (tui + tui2).
    
    Also adjust behavior for correctness:
    - Track a single pending rollback and block additional rollbacks until core responds.
    - Defer trimming transcript cells until ThreadRolledBack for the active session.
    - Clear the guard on ThreadRollbackFailed so the user can retry.
    - After a confirmed trim, schedule a one-shot scrollback refresh on the next draw.
    - Clear stale pending rollback state when switching sessions.
    
    ---------
    
    Co-authored-by: Josh McKinney <joshka@openai.com>
  • Use thread rollback for Esc backtrack (#9140)
    - Swap Esc backtrack to roll back the current thread instead of forking
  • fix(tui): show in-flight coalesced tool calls in transcript overlay (#8246)
    ### Problem
    Ctrl+T transcript overlay can omit in-flight coalesced tool calls because it
    renders only committed transcript cells while the main viewport can render the
    current in-flight ChatWidget.active_cell immediately.
    
    ### Mental model
    The UI has both committed transcript cells (finalized HistoryCell entries) and
    an in-flight active cell that can mutate in place while streaming, often
    representing a coalesced exec/tool group. The transcript overlay renders
    committed cells plus a render-only live tail derived from the current active
    cell. The live tail is cached and only recomputed when its cache key changes,
    which is derived from terminal width (wrapping), active-cell revision
    (in-place mutations), stream continuation (spacing), and animation tick
    (time-based visuals).
    
    ### Non-goals
    This does not change coalescing rules, flush boundaries, or when active cells
    become committed. It does not change tool-call semantics or transcript
    persistence; it is a rendering-only improvement for the overlay.
    
    ### Tradeoffs
    This adds cache invalidation complexity: correctness depends on bumping an
    active-cell revision (and/or providing an animation tick) when the active cell
    mutates in place. The mechanism is implemented in both codex-tui and codex-tui2,
    which keeps behavior consistent but risks drift if future changes are not
    applied in lockstep.
    
    ### Architecture
    App special-cases transcript overlay draws to sync a live tail from ChatWidget
    into TranscriptOverlay. TranscriptOverlay remains the owner of committed
    transcript cells; the live tail is an optional appended renderable.
    HistoryCell::transcript_animation_tick() allows time-dependent transcript output
    (spinner/shimmer) to invalidate the cached tail without requiring data mutation.
    
    ### Observability
    Manual verification is to open Ctrl+T while an exploring/coalesced active cell
    is still in-flight and confirm the overlay includes the same in-flight tool-call
    group the main viewport shows. The overlay is kept in sync by App passing an
    active-cell key and transcript lines into TranscriptOverlay::sync_live_tail; the
    key must change when the active cell mutates or animates.
    
    ### Tests
    Snapshot tests validate that the transcript overlay renders a live tail appended
    after committed cells and that identical keys short-circuit recomputation. Unit
    tests validate that active-cell revision bumps occur on specific in-place
    mutations (e.g. unified exec wait cell command display becoming known late) so
    cached tails are invalidated.
    
    ## Documentation patches (module, type, function)
    
    ### Module-level docs (invariants + mechanisms)
    - codex-rs/tui/src/app_backtrack.rs:1
    - codex-rs/tui/src/chatwidget.rs:1
    - codex-rs/tui/src/pager_overlay.rs:1
    - codex-rs/tui/src/history_cell.rs:1
    - codex-rs/tui2/src/app_backtrack.rs:1
    - codex-rs/tui2/src/chatwidget.rs:1
    - codex-rs/tui2/src/pager_overlay.rs:1
    - codex-rs/tui2/src/history_cell.rs:1
    
    ### Type-level docs (cache key + invariants)
    - codex-rs/tui/src/chatwidget.rs (ChatWidget.active_cell_revision, ActiveCellTranscriptKey)
    - codex-rs/tui/src/pager_overlay.rs (TranscriptOverlay live tail storage model)
    - codex-rs/tui/src/history_cell.rs (HistoryCell::transcript_animation_tick, UnifiedExecWaitCell::update_command_display)
    - Mirrored in codex-rs/tui2/src/chatwidget.rs, codex-rs/tui2/src/pager_overlay.rs, codex-rs/tui2/src/history_cell.rs
    
    ### Function-level docs (why/when/guarantees/pitfalls)
    - codex-rs/tui/src/app_backtrack.rs (overlay_forward_event)
    - codex-rs/tui/src/chatwidget.rs (active_cell_transcript_key, active_cell_transcript_lines)
    - codex-rs/tui/src/pager_overlay.rs (sync_live_tail, take_live_tail_renderable)
    - codex-rs/tui/src/history_cell.rs (transcript_animation_tick, UnifiedExecWaitCell::update_command_display)
    - Mirrored in codex-rs/tui2 equivalents where present
    
    ### Validation performed
    - cd codex-rs && just fmt
    - cd codex-rs && cargo test -p codex-tui
    - cd codex-rs && cargo test -p codex-tui2
    
    ## Design inconsistencies / risks
    
    - Cache invalidation is a distributed responsibility: any future in-place active
      cell transcript mutation that forgets to bump active_cell_revision (or expose
      an animation tick) can leave the transcript overlay live tail out of sync with
      the main viewport.
    - TranscriptOverlay tail handling assumes a structural invariant that the live
      tail, when present, is exactly one trailing renderable after the committed cell
      renderables; if renderable construction changes in a way that violates that
      assumption, tail insertion/removal logic becomes incorrect.
    - codex-tui and codex-tui2 duplicate the live-tail mechanism; the documentation
      is aligned, but the implementation can still drift unless changes continue to
      be applied in lockstep.
  • chore: unify conversation with thread name (#8830)
    Done and verified by Codex + refactor feature of RustRover
  • Remove model family from tui (#8488)
    - Remove model family from tui
  • 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.
  • 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
  • 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
  • Migrate tui to use models manager (#7555)
    - This PR treats the `ModelsManager` like `AuthManager` and propagate it
    into the tui, replacing the `builtin_model_presets`
    - We are also decreasing the visibility of `builtin_model_presets`
    
    based on https://github.com/openai/codex/pull/7552
  • feat: Support listing and selecting skills via $ or /skills (#7506)
    List/Select skills with $-mention or /skills
  • feat: codex tool tips (#7440)
    <img width="551" height="316" alt="Screenshot 2025-12-01 at 12 22 26"
    src="https://github.com/user-attachments/assets/6ca3deff-8ef8-4f74-a8e1-e5ea13fd6740"
    />
  • tui: fix backtracking past /status (#6335)
    Fixes https://github.com/openai/codex/issues/4722
    
    Supersedes https://github.com/openai/codex/pull/5058
    
    Ideally we'd have a clearer way of separating history per-session than
    by detecting a specific history cell type, but this is a fairly minimal
    fix for now.
  • Keep backtrack Esc hint gated on empty composer (#5076)
    ## Summary
    - only prime backtrack and show the ESC hint when the composer is empty
    - keep the composer-side ESC hint unchanged when drafts or attachments
    exist and cover it with a regression test
    
    Fixes #5030
    
    ------
    https://chatgpt.com/codex/tasks/task_i_68e95ba59cd8832caec8e72ae2efeb55
  • tui: bring the transcript closer to display mode (#4848)
    before
    <img width="1161" height="836" alt="Screenshot 2025-10-06 at 3 06 52 PM"
    src="https://github.com/user-attachments/assets/7622fd6b-9d37-402f-8651-61c2c55dcbc6"
    />
    
    after
    <img width="1161" height="858" alt="Screenshot 2025-10-06 at 3 07 02 PM"
    src="https://github.com/user-attachments/assets/1498f327-1d1a-4630-951f-7ca371ab0139"
    />
  • fix: remove mcp-types from app server protocol (#4537)
    We continue the separation between `codex app-server` and `codex
    mcp-server`.
    
    In particular, we introduce a new crate, `codex-app-server-protocol`,
    and migrate `codex-rs/protocol/src/mcp_protocol.rs` into it, renaming it
    `codex-rs/app-server-protocol/src/protocol.rs`.
    
    Because `ConversationId` was defined in `mcp_protocol.rs`, we move it
    into its own file, `codex-rs/protocol/src/conversation_id.rs`, and
    because it is referenced in a ton of places, we have to touch a lot of
    files as part of this PR.
    
    We also decide to get away from proper JSON-RPC 2.0 semantics, so we
    also introduce `codex-rs/app-server-protocol/src/jsonrpc_lite.rs`, which
    is basically the same `JSONRPCMessage` type defined in `mcp-types`
    except with all of the `"jsonrpc": "2.0"` removed.
    
    Getting rid of `"jsonrpc": "2.0"` makes our serialization logic
    considerably simpler, as we can lean heavier on serde to serialize
    directly into the wire format that we use now.
  • update composer + user message styling (#4240)
    Changes:
    
    - the composer and user messages now have a colored background that
    stretches the entire width of the terminal.
    - the prompt character was changed from a cyan `▌` to a bold `›`.
    - the "working" shimmer now follows the "dark gray" color of the
    terminal, better matching the terminal's color scheme
    
    | Terminal + Background        | Screenshot |
    |------------------------------|------------|
    | iTerm with dark bg | <img width="810" height="641" alt="Screenshot
    2025-09-25 at 11 44 52 AM"
    src="https://github.com/user-attachments/assets/1317e579-64a9-4785-93e6-98b0258f5d92"
    /> |
    | iTerm with light bg | <img width="845" height="540" alt="Screenshot
    2025-09-25 at 11 46 29 AM"
    src="https://github.com/user-attachments/assets/e671d490-c747-4460-af0b-3f8d7f7a6b8e"
    /> |
    | iTerm with color bg | <img width="825" height="564" alt="Screenshot
    2025-09-25 at 11 47 12 AM"
    src="https://github.com/user-attachments/assets/141cda1b-1164-41d5-87da-3be11e6a3063"
    /> |
    | Terminal.app with dark bg | <img width="577" height="367"
    alt="Screenshot 2025-09-25 at 11 45 22 AM"
    src="https://github.com/user-attachments/assets/93fc4781-99f7-4ee7-9c8e-3db3cd854fe5"
    /> |
    | Terminal.app with light bg | <img width="577" height="367"
    alt="Screenshot 2025-09-25 at 11 46 04 AM"
    src="https://github.com/user-attachments/assets/19bf6a3c-91e0-447b-9667-b8033f512219"
    /> |
    | Terminal.app with color bg | <img width="577" height="367"
    alt="Screenshot 2025-09-25 at 11 45 50 AM"
    src="https://github.com/user-attachments/assets/dd7c4b5b-342e-4028-8140-f4e65752bd0b"
    /> |
  • Tui: fix backtracking (#4020)
    Backtracking multiple times could drop earlier turns. We now derive the
    active user-turn positions from the transcript on demand (keying off the
    latest session header) instead of caching state. This keeps the replayed
    context intact during repeated edits and adds a regression test.
  • Revert "refactor transcript view to handle HistoryCells" (#3614)
    Reverts openai/codex#3538
    It panics on forking first message. It also calculates the index in a
    wrong way.
  • refactor transcript view to handle HistoryCells (#3538)
    No (intended) functional change.
    
    This refactors the transcript view to hold a list of HistoryCells
    instead of a list of Lines. This simplifies and makes much of the logic
    more robust, as well as laying the groundwork for future changes, e.g.
    live-updating history cells in the transcript.
    
    Similar to #2879 in goal. Fixes #2755.
  • Change forking to read the rollout from file (#3440)
    This PR changes get history op to get path. Then, forking will use a
    path. This will help us have one unified codepath for resuming/forking
    conversations. Will also help in having rollout history in order. It
    also fixes a bug where you won't see the UI when resuming after forking.
  • Generate more typescript types and return conversation id with ConversationSummary (#3219)
    This PR does multiple things that are necessary for conversation resume
    to work from the extension. I wanted to make sure everything worked so
    these changes wound up in one PR:
    1. Generate more ts types
    2. Resume rollout history files rather than create a new one every time
    it is resumed so you don't see a duplicate conversation in history for
    every resume. Chatted with @aibrahim-oai to verify this
    3. Return conversation_id in conversation summaries
    4. [Cleanup] Use serde and strong types for a lot of the rollout file
    parsing
  • Use ConversationId instead of raw Uuids (#3282)
    We're trying to migrate from `session_id: Uuid` to `conversation_id:
    ConversationId`. Not only does this give us more type safety but it
    unifies our terminology across Codex and with the implementation of
    session resuming, a conversation (which can span multiple sessions) is
    more appropriate.
    
    I started this impl on https://github.com/openai/codex/pull/3219 as part
    of getting resume working in the extension but it's big enough that it
    should be broken out.
  • TUI: Add session resume picker (--resume) and quick resume (--continue) (#3135)
    Adds a TUI resume flow with an interactive picker and quick resume.
    
    - CLI: 
      - --resume / -r: open picker to resume a prior session
      - --continue   / -l: resume the most recent session (no picker)
    - Behavior on resume: initial history is replayed, welcome banner
    hidden, and the first redraw is suppressed to avoid flicker.
    - Implementation:
    - New tui/src/resume_picker.rs (paginated listing via
    RolloutRecorder::list_conversations)
      - App::run accepts ResumeSelection; resumes from disk when requested
    - ChatWidget refactor with ChatWidgetInit and new_from_existing; replays
    initial messages
    - Tests: cover picker sorting/preview extraction and resumed-history
    rendering.
    - Docs: getting-started updated with flags and picker usage.
    
    
    
    https://github.com/user-attachments/assets/1bb6469b-e5d1-42f6-bec6-b1ae6debda3b
  • fix transcript lines being added to diff view (#2721)
    This fixes a bug where if you ran /diff while at turn was running,
    transcript lines would be added to the end of the diff view. Also,
    refactor to make this kind of issue less likely in future.
  • do not schedule frames for Tui::Draw events in backtrack (#2692)
    this was causing continuous rerendering when a transcript overlay was
    present