Commit Graph

13 Commits

  • feat(tui): allow function keys through f24 in keymaps (#25329)
    ## Why
    
    Closes #25006.
    
    `tui.keymap` currently rejects `F13` even though Codex's terminal event
    layer can report higher function keys. This prevents users from using
    common remappings such as Caps Lock to `F13`.
    
    ## What Changed
    
    - Define a shared portable upper bound of `F24` for stored TUI
    keybindings.
    - Accept `f13` through `f24` in config normalization and runtime
    parsing.
    - Allow `/keymap` capture to persist `F13` through `F24`.
    - Update the unsupported-function-key error and add boundary tests for
    `F13`, `F24`, and `F25`.
    
    ## How to Test
    
    1. Add a binding such as:
    
       ```toml
       [tui.keymap.global]
       open_transcript = "f13"
       ```
    
    2. Start Codex and press the remapped `F13` key.
    3. Confirm Codex loads the config without the previous `F1 through F12`
    error and the action runs.
    4. Open `/keymap`, capture `F13` for an action, and confirm the saved
    binding is `f13`.
    5. As a regression check, try to capture `F25` and confirm Codex reports
    that only `F1` through `F24` can be stored.
    
    Targeted tests:
    
    - `just test -p codex-config`
    - `just test -p codex-tui function_keys`
    
    Full `just test -p codex-tui` completed with 2,752 passing tests, 4
    skipped tests, and two unrelated guardian feature-flag failures:
    
    -
    `app::tests::update_feature_flags_disabling_guardian_clears_review_policy_and_restores_default`
    -
    `app::tests::update_feature_flags_disabling_guardian_clears_manual_review_policy_without_history`
  • [codex] Fix Vim normal mode editing (#25022)
    ## Summary
    - add Vim normal-mode `s` support to substitute the character under the
    cursor and enter insert mode
    - fix Vim normal-mode `o` so opening below the final line moves the
    cursor onto the new blank line
    - update keymap config/schema and keymap picker snapshots for the new
    action
    
    ## Validation
    - `just fmt`
    - `just write-config-schema`
    - `just test -p codex-config`
    - focused `just test -p codex-tui` coverage for the Vim `s` and `o`
    behavior, keymap conflict handling, and keymap picker snapshots
    - `cargo insta pending-snapshots --manifest-path tui/Cargo.toml`
    - `git diff --check`
    
    ## Notes
    A full `just test -p codex-tui` run still has two unrelated Guardian
    feature-flag failures in this checkout:
    -
    `app::tests::update_feature_flags_disabling_guardian_clears_review_policy_and_restores_default`
    -
    `app::tests::update_feature_flags_disabling_guardian_clears_manual_review_policy_without_history`
  • feat(tui): make turn interruption keybind configurable (#24766)
    ## Why
    
    Interrupting an active turn is currently fixed to `Esc`, which is easy
    to hit accidentally and cannot be customized through `/keymap`. This
    gives users a less accidental binding while preserving the existing
    default.
    
    ## What Changed
    
    - Adds `tui.keymap.chat.interrupt_turn` to `/keymap`, defaulting to
    `esc` and supporting remapping or unbinding.
    - Uses the configured interrupt binding for running-turn status, queued
    steer interruption, and `request_user_input`, including the visible
    hints.
    - Preserves local `Esc` behavior for popups, Vim insert mode, and
    `/agent` editing while validating conflicts with fixed/backtrack and
    request-input navigation bindings.
    - Adds behavior and snapshot coverage for remapped interruption paths.
    
    ## How to Test
    
    1. Run Codex and open `/keymap`, then set **Interrupt Turn** to `f12`.
    2. Start a turn and confirm `Esc` no longer interrupts it while `f12`
    does; the running hint should display `f12 to interrupt`.
    3. Queue a steer while a turn is running and confirm the preview
    displays `f12`; pressing it should interrupt and submit the steer
    immediately.
    4. Trigger a `request_user_input` prompt and confirm its footer uses
    `f12`; with notes open, `Esc` should still clear notes while `f12`
    interrupts the turn.
    5. Clear the Interrupt Turn binding and confirm the key-specific
    interrupt hint is removed while `Ctrl+C` remains available.
    
    Targeted validation:
    
    - `just write-config-schema`
    - `just fix -p codex-config`
    - `just fix -p codex-tui`
    - `just fmt`
    - `just argument-comment-lint-from-source -p codex-config -p codex-tui`
    - `just test -p codex-config`
    - `cargo insta pending-snapshots --manifest-path tui/Cargo.toml`
    - `just test -p codex-tui keymap_setup::tests`
    - `just test -p codex-tui` (fails in two pre-existing guardian
    feature-flag tests unrelated to this diff; the intentional picker
    snapshot updates were reviewed and accepted)
  • feat(tui): add vim text object bindings (#24382)
    ## Why
    
    Vim mode currently supports some normal-mode operators and motions, but
    common text-object combinations like `ciw`, `daw`, `di(`, and
    quote/bracket variants are still missing. That makes the composer feel
    incomplete for users who expect operator + text object editing to work
    inside prompts.
    
    Closes #21383.
    
    ## What Changed
    
    - Add Vim pending-state support for operator/text-object sequences.
    - Add `c` as a normal-mode operator for text objects, so combinations
    like `ciw` delete the object and enter insert mode.
    - Support word, WORD, delimiter, and quote text objects:
      - `iw`, `aw`, `iW`, `aW`
      - `i(`, `a(`, `i)`, `a)`, `ib`, `ab`
      - `i[`, `a[`, `i]`, `a]`
      - `i{`, `a{`, `i}`, `a}`, `iB`, `aB`
      - `i"`, `a"`, `i'`, `a'`, `i\``, `a\``
    - Add configurable keymap entries and keymap picker coverage for the new
    Vim text-object context.
    - Regenerate the config schema and update keymap picker snapshots.
    
    ## How to Test
    
    Manual smoke test:
    
    1. Start Codex with Vim composer mode enabled.
    2. Type a draft such as:
       ```text
       alpha beta gamma
       call(foo[bar], {"x": "hello world"})
       say "one \"two\" three" now
       ```
    3. Put the cursor on `beta`, press `ciw`, and confirm `beta` is removed
    and the composer enters insert mode.
    4. Escape back to normal mode, put the cursor on `gamma`, press `daw`,
    and confirm `gamma` plus surrounding whitespace is removed.
    5. Put the cursor inside `foo[bar]`, press `di[`, and confirm only `bar`
    is removed.
    6. Put the cursor inside `call(...)`, press `da(`, and confirm the whole
    parenthesized section is removed.
    7. Put the cursor inside the quoted text, press `ci"`, and confirm the
    quote contents are removed and insert mode starts.
    8. Verify cancellation does not edit text: press `d` then `Esc`, and
    press `d` then `i` then `Esc`.
    
    Targeted tests:
    
    - `cargo test -p codex-tui --lib vim_`
    - `cargo nextest run -p codex-tui keymap_setup::tests`
    
    Additional local checks:
    
    - `just write-config-schema`
    - `just fmt`
    - `just fix -p codex-tui`
    - `git diff --check`
    - `cargo insta pending-snapshots --manifest-path tui/Cargo.toml`
    
    Local full-suite note: `just test -p codex-tui` ran to completion. The
    keymap snapshot failures were expected and accepted. Two unrelated
    guardian feature-flag tests still fail locally:
    -
    `app::tests::update_feature_flags_disabling_guardian_clears_review_policy_and_restores_default`
    -
    `app::tests::update_feature_flags_disabling_guardian_clears_manual_review_policy_without_history`
    
    `just argument-comment-lint` is currently blocked locally by Bazel
    analysis before the lint runs because `compiler-rt` has an empty
    `include/sanitizer/*.h` glob in the local Bazel cache. The touched Rust
    diff was manually inspected for opaque positional literals.
  • fix(tui): complete vim word-end and line-end behavior (#24380)
    ## Why
    
    The TUI Vim composer currently diverges from normal Vim editing in two
    common workflows: pressing `e` repeatedly can remain stuck at an
    existing word end, and normal mode does not support `C` for changing
    through the end of the line. The existing `D` behavior also removes the
    newline when the cursor is already at the line boundary, which makes the
    new `C` action and existing deletion action surprising in multiline
    prompts.
    
    Closes #23926.
    Closes #24238.
    
    ## What Changed
    
    - Make normal-mode `e` advance from the current word end to the next
    word end, including for operator motions such as `de`.
    - Add configurable Vim normal-mode `change_to_line_end` behavior, bound
    to `C` by default, which deletes to the end of the current line and
    enters Insert mode.
    - Keep the newline intact when `D` or `C` is pressed at the end-of-line
    boundary.
    - Add regression coverage for repeated `e`, `de`, `C`, and the multiline
    `C`/`D` boundary behavior.
    - Regenerate the config schema and update the keymap picker snapshots
    for the new Vim action.
    
    ## How to Test
    
    1. Run Codex with Vim composer mode enabled:
       ```bash
       cd codex-rs
       cargo run --bin codex -- -c tui.vim_mode_default=true
       ```
    2. Enter `alpha beta gamma`, press `Esc`, `0`, then press `e`
    repeatedly.
    Confirm the cursor advances through the ends of `alpha`, `beta`, and
    `gamma`.
    3. Enter `hello world`, press `Esc`, `0`, `w`, then `C`.
       Confirm `world` is deleted and the composer enters Insert mode.
    4. Enter a multiline prompt with `hello` above `world`, press `Esc`,
    `k`, `$`, and then `D`.
       Confirm the newline is preserved and the two lines do not join.
    5. At the same boundary, press `C` and type `!`.
    Confirm the composer enters Insert mode and yields `hello!` above
    `world`, preserving the newline.
    
    Targeted automated verification:
    - `just fix -p codex-tui`
    - `just argument-comment-lint-from-source -p codex-tui -p codex-config`
    - `cargo insta pending-snapshots` reports no pending snapshots.
    - `just test -p codex-tui` validates the new Vim and keymap snapshot
    coverage, but the command remains red due to two reproducible unrelated
    failures in `app::tests::update_feature_flags_disabling_guardian_*`.
    
    ## Validation Note
    
    The workspace-wide `just argument-comment-lint` form is currently
    blocked during Bazel analysis by the existing LLVM `compiler-rt` missing
    `include/sanitizer/*.h` failure; package-scoped source linting for the
    changed Rust crates passed.
  • Prevent Esc from dismissing or rewinding /side (#22710)
    Addresses #22599
    
    ## Why
    `/side` currently lets `Esc` return to the parent thread. Multiple users
    reported that this collides with queued-steer UI that also advertises
    `Esc`, so a timing-sensitive keypress can dismiss an ephemeral side chat
    instead of sending the queued prompt.
    
    After removing that dismissal shortcut, the same `Esc` path could fall
    through to main-thread backtrack/edit-previous handling, which is not
    valid for ephemeral side conversations. This keeps `/side` out of both
    global `Esc` behaviors.
    
    ## What changed
    - Remove `Esc` from the `/side` return shortcut matcher while keeping
    the existing `Ctrl+C` and `Ctrl+D` behavior.
    - Update side-conversation hints and blocked-command copy to advertise
    `Ctrl+C` as the return shortcut.
    - Rename the reserved `Esc` keymap label to describe backtracking only.
    - Block backtrack/edit-previous handling while a side conversation is
    active and report `Editing previous prompts is unavailable in side
    conversations.` when that path would have fired.
    - Keep composer-owned `Esc` behavior, such as Vim insert-mode escape,
    routed locally.
    - Refresh focused shortcut assertions and TUI snapshots for the updated
    footer and new side-conversation error message.
    
    ## Verification
    Manually tested `/side` use cases and `Esc`, `Ctrl+C`, `Ctrl+D`.
  • feat(tui): standardize picker navigation keys (#22347)
    ## Why
    
    Picker-style UI in the TUI has accumulated a mix of hardcoded navigation
    keys. Some lists supported page movement, some did not; some accepted
    Vim-like keys, while others only accepted arrows; and tabbed or
    horizontally adjustable pickers had no shared keymap action for
    left/right movement.
    
    This PR makes picker/list navigation consistent and configurable so
    users can rely on the same defaults across the TUI.
    
    ## What Changed
    
    - Adds shared list keymap actions for:
      - vertical movement: `move_up`, `move_down`
      - horizontal movement: `move_left`, `move_right`
      - paging and jumps: `page_up`, `page_down`, `jump_top`, `jump_bottom`
    - Adds defaults:
    - Up/down: arrows, `Ctrl+P/N`, `Ctrl+K/J`, and plain `k/j` where text
    input is not active
      - Page up/down: `PageUp/PageDown` and `Ctrl+B/F`
      - First/last: `Home/End`
      - Left/right: `Left/Right` and `Ctrl+H/L`
    - Wires the shared list keymap through picker and list surfaces
    including session resume, multi-select, tabbed selection lists,
    settings-style lists, app-link selection, MCP elicitation,
    request-user-input, and the OSS selection wizard.
    - Keeps search behavior intact by reserving printable characters for
    query text in searchable pickers.
    - Updates keymap setup actions, config schema, snapshots, and focused
    coverage for the new list actions.
    
    ## How to Test
    
    1. Start Codex from this branch and open the session picker, for example
    with an existing session history.
    2. In the session list, verify that `Ctrl+J/K` moves the selection
    down/up.
    3. Verify that `Ctrl+F/B` pages down/up and `Home/End` jumps to the
    first/last visible session.
    4. Type printable search text such as `j` or `k` and confirm it updates
    the query instead of navigating.
    5. Focus a picker control that changes values horizontally, such as a
    session picker toolbar control, and verify `Ctrl+H/L` changes the
    focused value like left/right arrows.
    
    Targeted tests run:
    
    - `cargo test -p codex-tui keymap::tests::`
    - `cargo test -p codex-tui keymap_setup::tests::`
    - `cargo test -p codex-tui horizontal_list_keys`
    - `cargo test -p codex-tui page_and_jump_navigation_use_list_keymap`
    - `cargo test -p codex-tui ctrl_h_l_move_provider_selection`
    - `cargo test -p codex-tui scroll_state::tests`
    - `cargo test -p codex-tui
    switching_tabs_changes_visible_items_and_clears_search`
    - `cargo test -p codex-tui toggle_sort_key_reloads_with_new_sort`
    
    Also ran `just write-config-schema`, `just fmt`, `just fix -p
    codex-tui`, `just argument-comment-lint`, and `git diff --check`.
    
    Note: `cargo test -p codex-tui` was attempted and still aborts in the
    pre-existing
    `tests::fork_last_filters_latest_session_by_cwd_unless_show_all` stack
    overflow, which is unrelated to this branch.
  • feat(tui): add raw scrollback mode (#20819)
    ## Why
    
    Granular copy is particularly difficult with the current output. Part of
    it was solved with the introduction of the `/copy` command but when you
    only need to copy parts of a response, you still encounter some issues:
    
    - When you copy a paragraph, the result is a sequence of separate lines
    instead of one correctly joined paragraph.
    - When a word wraps, part of it stays on the original line and the rest
    appears at the start of the next line.
    - When you copy a long command, extra line breaks are often inserted,
    and command arguments can be split across multiple lines.
    
    
    https://github.com/user-attachments/assets/0ef85c84-9363-4aad-b43a-15fce062a443
    
    ## Solution
    
    Now that we own the scrollback and we re-create it when we resize, we
    have the opportunity of toggling between the raw text and the rich text
    we see today.
    
    - Add TUI raw scrollback mode with `tui.raw_output_mode`, `/raw
    [on|off]`, and the configurable `tui.keymap.global.toggle_raw_output`
    action.
    - Render transcript cells through rich/raw-aware paths so raw mode
    preserves source text and lets the terminal soft-wrap selection-friendly
    output.
    - Bind raw-mode toggle to `alt-r` by default, with the keybinding path
    toggling silently while `/raw` continues to emit confirmation messages.
    
    ## Related Issues
    
    Likely addressed by raw mode:
    
    - #12200: clean copy for multiline and soft-wrapped output. Raw mode
    removes Codex-inserted wrapping/indentation and lets the terminal
    soft-wrap logical lines.
    - #9252: command suggestions gain unwanted leading spaces when copied.
    Raw mode renders transcript text without the rich-mode left
    padding/gutter.
    - #8258: prompt output is hard to copy because of leading indentation.
    Raw mode renders user/source-backed transcript text without that
    decorative indentation.
    
    Partially or conditionally addressed:
    
    - #2880: copy/export message as Markdown. Raw mode exposes raw Markdown
    for terminal selection, but this PR does not add a dedicated
    export/copy-message command.
    - #19820: mouse drag selection + copy in the TUI. Raw mode improves
    terminal-native selection of output/history text, but this PR does not
    implement in-TUI mouse selection, highlighting, auto-copy, or composer
    selection.
    - #18979: copied content is divided into two parts. This should improve
    cases caused by Codex-inserted wraps/padding in rendered output; if the
    report is about pasting into the composer/input path, that remains
    outside this PR.
    
    ## Validation
    
    - `just write-config-schema`
    - `just fmt`
    - `cargo test -p codex-config`
    - `cargo test -p codex-tui`
    - `just fix -p codex-tui`
    - `just argument-comment-lint`
    - `cargo test -p codex-tui
    raw_output_mode_can_change_without_inserting_notice -- --nocapture`
    - `cargo test -p codex-tui
    raw_slash_command_toggles_and_accepts_on_off_args -- --nocapture`
    - `cargo test -p codex-tui raw_output_toggle -- --nocapture`
    - `git diff --check`
    - `cargo insta pending-snapshots`
  • fix(tui): support modified backspace/delete keys (#21058)
    ## Why
    
    Fixes #21046.
    
    Codex TUI 0.128.0 can show Backspace/Delete-related editor shortcuts in
    `/keymap`, but Windows-style modified Backspace/Delete events were still
    dropped by the composer because the default editor keymap did not
    include those modified special-key variants. On Windows/CMD this meant
    `Shift+Backspace` and `Shift+Delete` did not fall through to normal
    character deletion, and `Ctrl+Backspace` / `Ctrl+Delete` did not perform
    the word deletion users expect from Windows text inputs.
    
    ## What Changed
    
    - Added default editor bindings for `shift-backspace` and `shift-delete`
    so shifted delete keys keep normal grapheme deletion behavior.
    - Added default editor bindings for `ctrl-backspace`,
    `ctrl-shift-backspace`, `ctrl-delete`, and `ctrl-shift-delete` so
    Windows-style word deletion works when terminals preserve those
    modifiers.
    - Added regression coverage for the resolved default keymap and textarea
    behavior.
    
    ## How to Test
    
    1. Start Codex in the TUI on Windows CMD or another terminal that
    reports modified Backspace/Delete keys distinctly.
    2. Type `hello world` in the composer.
    3. Press `Ctrl+Backspace`; confirm `world` is removed and `hello `
    remains.
    4. Type `world` again, move the cursor before it, then press
    `Ctrl+Delete`; confirm the next word is removed.
    5. Type a few characters and press `Shift+Backspace` and `Shift+Delete`;
    confirm they delete one character in the expected direction instead of
    doing nothing.
    6. Open `/keymap`, inspect the Editor deletion actions, and confirm the
    modified Backspace/Delete aliases are visible as configurable defaults.
    
    Targeted tests:
    - `cargo test -p codex-tui keymap::tests`
    - `cargo test -p codex-tui bottom_pane::textarea::tests`
    - `cargo test -p codex-tui keymap_setup::tests`
  • feat(tui): improve TUI keymap coverage (#20798)
    ## Summary
    - normalize terminal-emitted C0 control characters through configurable
    editor keymaps, covering raw control-key fallbacks like
    Shift+Enter-as-LF in terminals from #20555 and #20898, plus part of the
    modified-Enter behavior in #20580
    - add default-unbound keymap actions for toggling Fast mode and killing
    the current composer line, giving #20698 users a configurable zsh-style
    Ctrl+U option without changing the existing default Ctrl+U behavior
    - wire the new actions through gated /keymap picker entries, schema
    generation, and snapshot coverage
    
    Fixes #20555.
    Fixes #20898.
    
    ## Testing
    - just write-config-schema
    - just fmt
    - cargo test -p codex-config
    - cargo test -p codex-tui keymap::tests
    - cargo test -p codex-tui bottom_pane::textarea::tests
    - cargo test -p codex-tui keymap_setup::tests
    - cargo insta pending-snapshots
    - just fix -p codex-tui
    - git diff --check
    - just argument-comment-lint
  • fix(tui): restore alt-enter newline alias (#20535)
    Fixes https://github.com/openai/codex/issues/20501
    
    ## Summary
    - add Alt+Enter to the built-in editor newline aliases
    - update keymap tests that used Alt+Enter as a custom submit binding now
    that it conflicts with newline
    - refresh the keymap action-menu snapshot fixture
    
    ## Test Plan
    - `just fmt`
    - `cargo test -p codex-tui keymap::tests`
    - `cargo test -p codex-tui bottom_pane::textarea::tests`
    - `cargo test -p codex-tui keymap_setup::tests`
    - `cargo test -p codex-tui`
    - `cargo insta pending-snapshots`
    - `git diff --check`
    - `just argument-comment-lint`
  • feat(tui): add vim composer mode (#18595)
    ## Why
    
    Codex now has configurable TUI keymaps, but the composer still behaves
    like a plain text field. Users who prefer modal editing need a way to
    keep Vim muscle memory while drafting prompts, and the keymap picker
    needs to expose Vim-specific actions if those bindings are configurable
    instead of hardcoded.
    
    ## What Changed
    
    - Adds composer Vim mode with insert/normal state, common normal-mode
    movement and editing commands, `d`/`y` operator-pending flows, and
    mode-aware footer and cursor indicators.
    - Adds `/vim`, an optional global `toggle_vim_mode` binding, and
    `tui.vim_mode_default` so Vim mode can be toggled per session or enabled
    as the default composer state.
    - Extends runtime and config keymaps with `vim_normal` and
    `vim_operator` contexts, exposes those contexts in `/keymap`, refreshes
    the config schema, and validates Vim bindings separately.
    - Integrates Vim normal mode with existing composer behavior: `/` opens
    slash command entry, `!` enters shell mode, `j`/`k` navigate history at
    history boundaries, successful submissions reset back to normal mode,
    and paste burst handling remains insert-mode only.
    - Teaches the TUI render path to apply and restore cursor style so Vim
    insert mode can use a bar cursor without leaving the terminal in that
    state after exit.
    
    ## Validation
    
    - `cargo test -p codex-tui keymap -- --nocapture` on the keymap/Vim
    coverage
    - `cargo insta pending-snapshots`
    
    ## Docs
    
    This introduces user-facing `/vim`, `tui.vim_mode_default`, and Vim
    keymap contexts under `tui.keymap`, so the public CLI configuration and
    slash-command docs should be updated before the feature ships.
  • 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.