Commit Graph

931 Commits

  • Prompt Expansion: Preserve Text Elements (#9518)
    Summary
    - Preserve `text_elements` through custom prompt argument parsing and
    expansion (named and numeric placeholders).
    - Translate text element ranges through Shlex parsing using sentinel
    substitution, and rehydrate text + element ranges per arg.
    - Drop image attachments when their placeholder does not survive prompt
    expansion, keeping attachments consistent with rendered elements.
    - Mirror changes in TUI2 and expand tests for prompt parsing/expansion
    edge cases.
    
    Tests
    - placeholders with spaces as single tokens (positional + key=value,
    quoted + unquoted),
      - prompt expansion with image placeholders,
      - large paste + image arg combinations,
      - unused image arg dropped after expansion.
  • Show session header before configuration (#9568)
    We were skipping if we know the model. We shouldn't
  • [codex-tui] exit when terminal is dumb (#9293)
    Using terminal with TERM=dumb specifically mean that TUIs and the like
    don't work. Ensure that codex doesn't run in these environments and exit
    with odd errors like crossterm's "Error: The cursor position could not
    be read within a normal duration"
    
    ---------
    
    Co-authored-by: Josh McKinney <joshka@openai.com>
  • Improve UI spacing for queued messages (#9162)
    Despite good spacing between queued messages and assistant message text:
    <img width="462" height="322" alt="Screenshot 2026-01-12 at 4 54 50 PM"
    src="https://github.com/user-attachments/assets/e8b46252-0b33-40d2-b431-cb73b9a3bd2e"
    />
    
    Codex has confusing spacing between queued messages and shimmering
    status text (making the queued message seem like a sub-item of the
    shimmering status text)
    <img width="615" height="217" alt="Screenshot 2026-01-12 at 4 54 18 PM"
    src="https://github.com/user-attachments/assets/ee5e6095-8fe9-4863-88d2-10472cab8bd6"
    />
    
    This PR changes the spacing between the queued message(s) and shimmering
    status text to make it less confusing:
    <img width="440" height="240" alt="Screenshot 2026-01-13 at 11 20 36 AM"
    src="https://github.com/user-attachments/assets/02dcc690-cbe9-4943-87de-c7300ef51120"
    />
    
    While working on the status/queued spacing change, we noticed two
    paste‑burst tests were timing‑sensitive and could fail
    on slower CI. We added a small test‑only helper to keep the paste‑burst
    state active and refreshed during these tests. This
    removes dependence on tight timing and makes the tests deterministic
    without affecting runtime behavior.
  • Add total (non-partial) TextElement placeholder accessors (#9545)
    ## Summary
    - Make `TextElement` placeholders private and add a text-backed accessor
    to avoid assuming `Some`.
    - Since they are optional in the protocol, we want to make sure any
    accessors properly handle the None case (getting the placeholder using
    the byte range in the text)
    - Preserve placeholders during protocol/app-server conversions using the
    accessor fallback.
    - Update TUI composer/remap logic and tests to use the new
    constructor/accessor.
  • Fixed config merging issue with profiles (#9509)
    This PR fixes a small issue with chained (layered) config.toml file
    merging. The old logic didn't properly handle profiles.
    
    In particular, if a lower-layer config overrides a profile defined in a
    higher-layer config, the override did not take effect. This prevents
    users from having project-specific profile overrides and contradicts the
    (soon-to-be) documented behavior of config merging.
    
    The change adds a unit test for this case. It also exposes a function
    from the config crate that is needed by the app server code paths to
    implement support for layered configs.
  • Tui: use collaboration mode instead of model and effort (#9507)
    - Only use collaboration modes in the tui state to track model and
    effort.
    - No behavior change without the collaboration modes flag.
    - Change model and effort on /model, /collab (behind a flag), and
    shift+tab (behind flag)
  • nit: do not render terminal interactions if no task running (#9374)
    To prevent race where the terminal interaction message is processed
    after the last message
  • fix(tui) fix user message light mode background (#9407)
    ## Summary
    Fixes the user message styles for light mode.
    
    ## Testing
    Attaching 2 screenshots from ghostty, but I also tried various styles in
    Terminal.app and iTerm2.
    **Before**
    <img width="888" height="560" alt="Screenshot 2026-01-16 at 5 22 36 PM"
    src="https://github.com/user-attachments/assets/73d9decb-a01a-4ece-b88e-ea49a33cc0c6"
    />
    
    **After**
    <img width="890" height="281" alt="Screenshot 2026-01-16 at 5 22 59 PM"
    src="https://github.com/user-attachments/assets/6689e286-d699-4ceb-b0cb-579a31b047bf"
    />
  • 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.
  • Migrate tui to use UserTurn (#9497)
    - `tui/` and `tui2/` submit `Op::UserTurn` and own full turn context
    (cwd/approval/sandbox/model/etc.).
    - `Op::UserInput` is documented as legacy in `codex-protocol` (doc-only;
    no `#[deprecated]` to avoid `-D warnings` fallout).
    - Remove obsolete `#[allow(deprecated)]` and the unused `ConversationId`
    alias/re-export.
  • tui: avoid Esc interrupt when skill popup active (#9451)
    Fixes #9450
    
    ## What
    - When a task is running and the skills autocomplete popup is open,
    `Esc` now dismisses the popup instead of sending `Op::Interrupt`.
    - `Esc` still interrupts a running task when no popup is active.
    
    ## Tests
    - `cargo test -p codex-tui`
    
    ---------
    
    Co-authored-by: prateek <199982+prateek@users.noreply.github.com>
  • Feat: request user input tool (#9472)
    ### Summary
    * Add `requestUserInput` tool that the model can use for gather
    feedback/asking question mid turn.
    
    
    ### Tool input schema
    ```
    {
      "$schema": "http://json-schema.org/draft-07/schema#",
      "title": "requestUserInput input",
      "type": "object",
      "additionalProperties": false,
      "required": ["questions"],
      "properties": {
        "questions": {
          "type": "array",
          "description": "Questions to show the user (1-3). Prefer 1 unless multiple independent decisions block progress.",
          "minItems": 1,
          "maxItems": 3,
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["id", "header", "question"],
            "properties": {
              "id": {
                "type": "string",
                "description": "Stable identifier for mapping answers (snake_case)."
              },
              "header": {
                "type": "string",
                "description": "Short header label shown in the UI (12 or fewer chars)."
              },
              "question": {
                "type": "string",
                "description": "Single-sentence prompt shown to the user."
              },
              "options": {
                "type": "array",
                "description": "Optional 2-3 mutually exclusive choices. Put the recommended option first and suffix its label with \"(Recommended)\". Only include \"Other\" option if we want to include a free form option. If the question is free form in nature, do not include any option.",
                "minItems": 2,
                "maxItems": 3,
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": ["value", "label", "description"],
                  "properties": {
                    "value": {
                      "type": "string",
                      "description": "Machine-readable value (snake_case)."
                    },
                    "label": {
                      "type": "string",
                      "description": "User-facing label (1-5 words)."
                    },
                    "description": {
                      "type": "string",
                      "description": "One short sentence explaining impact/tradeoff if selected."
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
    ```
    
    ### Tool output schema
    ```
    {
      "$schema": "http://json-schema.org/draft-07/schema#",
      "title": "requestUserInput output",
      "type": "object",
      "additionalProperties": false,
      "required": ["answers"],
      "properties": {
        "answers": {
          "type": "object",
          "description": "Map of question id to user answer.",
          "additionalProperties": {
            "type": "object",
            "additionalProperties": false,
            "required": ["selected"],
            "properties": {
              "selected": {
                "type": "array",
                "items": { "type": "string" }
              },
              "other": {
                "type": ["string", "null"]
              }
            }
          }
        }
      }
    }
    ```
  • TUI: collaboration mode UX + always submit UserTurn when enabled (#9461)
    - Adds experimental collaboration modes UX in TUI: Plan / Pair
    Programming / Execute.
    - Gated behind `Feature::CollaborationModes`; existing behavior remains
    unchanged when disabled.
    - Selection UX:
    - `Shift+Tab` cycles modes while idle (no task running, no modal/popup).
    - `/collab` cycles; `/collab <plan|pair|pp|execute|exec>` sets
    explicitly.
    - Footer flash after changes + shortcut overlay shows `Shift+Tab` “to
    change mode”.
      - `/status` shows “Collaboration mode”.
    - Submission semantics:
    - When enabled: every submit uses `Op::UserTurn` and always includes
    `collaboration_mode: Some(...)` (default Pair Programming).
      - Removes the one-shot “pending collaboration mode” behavior.
    - Implementation:
    - New `tui/src/collaboration_modes.rs` (selection enum/cycle, `/collab`
    parsing, resolve to `CollaborationMode`, footer flash line).
    - Fallback: `resolve_mode_or_fallback` synthesizes a `CollaborationMode`
    when presets are missing (uses current model + reasoning effort; no
    `developer_instructions`) to avoid core falling back to `Custom`.
      - TODO: migrate TUI to use `Op::UserTurn`.
  • Fixed TUI regression related to image paste in WSL (#9473)
    This affects quoted paths in WSL. The fix is to normalize quoted Windows
    paths before WSL conversion.
    
    This addresses #9456
  • 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>
  • Preserve slash command order in search (#9425)
    Keep slash popup search results in presentation order for built-ins and
    prompts.
  • chore(instructions) Remove unread SessionMeta.instructions field (#9423)
    ### Description
    - Remove the now-unused `instructions` field from the session metadata
    to simplify SessionMeta and stop propagating transient instruction text
    through the rollout recorder API. This was only saving
    user_instructions, and was never being read.
    - Stop passing user instructions into the rollout writer at session
    creation so the rollout header only contains canonical session metadata.
    
    ### Testing
    
    - Ran `just fmt` which completed successfully.
    - Ran `just fix -p codex-protocol`, `just fix -p codex-core`, `just fix
    -p codex-app-server`, `just fix -p codex-tui`, and `just fix -p
    codex-tui2` which completed (Clippy fixes applied) as part of
    verification.
    - Ran `cargo test -p codex-protocol` which passed (28 tests).
    - Ran `cargo test -p codex-core` which showed failures in a small set of
    tests (not caused by the protocol type change directly):
    `default_client::tests::test_create_client_sets_default_headers`,
    several `models_manager::manager::tests::refresh_available_models_*`,
    and `shell_snapshot::tests::linux_sh_snapshot_includes_sections` (these
    tests failed in this CI run).
    - Ran `cargo test -p codex-app-server` which reported several failing
    integration tests (including
    `suite::codex_message_processor_flow::test_codex_jsonrpc_conversation_flow`,
    `suite::output_schema::send_user_turn_*`, and
    `suite::user_agent::get_user_agent_returns_current_codex_user_agent`).
    - `cargo test -p codex-tui` and `cargo test -p codex-tui2` were
    attempted but aborted due to disk space exhaustion (`No space left on
    device`).
    
    ------
    [Codex
    Task](https://chatgpt.com/codex/tasks/task_i_696bd8ce632483228d298cf07c7eb41c)
  • 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>
  • Support enable/disable skill via config/api. (#9328)
    In config.toml:
    ```
    [[skills.config]]
    path = "/Users/xl/.codex/skills/my_skill/SKILL.md"
    enabled = false
    ```
    
    API:
    skills/list, skills/config/write
  • feat: show forked from session id in /status (#9330)
    Summary:
    - Add forked_from to SessionMeta/SessionConfiguredEvent and persist it
    for forked sessions.
    - Surface forked_from in /status for tui + tui2 and add snapshots.
  • feat(app-server, core): return threads by created_at or updated_at (#9247)
    Add support for returning threads by either `created_at` OR `updated_at`
    descending. Previously core always returned threads ordered by
    `created_at`.
    
    This PR:
    - updates core to be able to list threads by `updated_at` OR
    `created_at` descending based on what the caller wants
    - also update `thread/list` in app-server to expose this (default to
    `created_at` if not specified)
    
    All existing codepaths (app-server, TUI) still default to `created_at`,
    so no behavior change is expected with this PR.
    
    **Implementation**
    To sort by `updated_at` is a bit nontrivial (whereas `created_at` is
    easy due to the way we structure the folders and filenames on disk,
    which are all based on `created_at`).
    
    The most naive way to do this without introducing a cache file or sqlite
    DB (which we have to implement/maintain) is to scan files in reverse
    `created_at` order on disk, and look at the file's mtime (last modified
    timestamp according to the filesystem) until we reach `MAX_SCAN_FILES`
    (currently set to 10,000). Then, we can return the most recent N
    threads.
    
    Based on some quick and dirty benchmarking on my machine with ~1000
    rollout files, calling `thread/list` with limit 50, the `updated_at`
    path is slower as expected due to all the I/O:
    - updated-at: average 103.10 ms
    - created-at: average 41.10 ms
    
    Those absolute numbers aren't a big deal IMO, but we can certainly
    optimize this in a followup if needed by introducing more state stored
    on disk.
    
    **Caveat**
    There's also a limitation in that any files older than `MAX_SCAN_FILES`
    will be excluded, which means if a user continues a REALLY old thread,
    it's possible to not be included. In practice that should not be too big
    of an issue.
    
    If a user makes...
    - 1000 rollouts/day → threads older than 10 days won't show up
    - 100 rollouts/day → ~100 days
    
    If this becomes a problem for some reason, even more motivation to
    implement an updated_at cache.
  • feat: /fork the current session instead of opening session picker (#9385)
    Implemented /fork to fork the current session directly (no picker),
    handling it via a new ForkCurrentSession app event in both tui and tui2.
    Updated slash command descriptions/tooltips and adjusted the fork tests
    accordingly. Removed the unused in-session fork picker event.
  • chore: upgrade to Rust 1.92.0 (#8860)
    **Summary**
    - Upgrade Rust toolchain used by CI to 1.92.0.
    - Address new clippy `derivable_impls` warnings by deriving `Default`
    for enums across protocol, core, backend openapi models, and
    windows-sandbox setup.
    - Tidy up related test/config behavior (originator header handling, env
    override cleanup) and remove a now-unused assignment in TUI/TUI2 render
    layout.
    
    **Testing**
    - `just fmt`
    - `just fix -p codex-tui`
    - `just fix -p codex-tui2`
    - `just fix -p codex-windows-sandbox`
    - `cargo test -p codex-tui`
    - `cargo test -p codex-tui2`
    - `cargo test -p codex-windows-sandbox`
    - `cargo test -p codex-core --test all`
    - `cargo test -p codex-app-server --test all`
    - `cargo test -p codex-mcp-server --test all`
    - `cargo test --all-features`
  • feat: propagate approval request of unsubscribed threads (#9232)
    A thread can now be spawned by another thread. In order to process the
    approval requests of such sub-threads, we need to detect those event and
    show them in the TUI.
    
    This is a temporary solution while the UX is being figured out. This PR
    should be reverted once done
  • fix(tui): only show 'Worked for' separator when actual work was performed (#8958)
    Fixes #7919.
    
    This PR addresses a TUI display bug where the "Worked for" separator
    would appear prematurely during the planning stage.
    
    **Changes:**
    - Added `had_work_activity` flag to `ChatWidget` to track if actual work
    (exec commands, MCP tool calls, patches) was performed in the current
    turn.
    - Updated `handle_streaming_delta` to only display the
    `FinalMessageSeparator` if both `needs_final_message_separator` AND
    `had_work_activity` are true.
    - Updated `handle_exec_end_now`, `handle_patch_apply_end_now`, and
    `handle_mcp_end_now` to set `had_work_activity = true`.
    
    **Verification:**
    - Ran `cargo test -p codex-tui` to ensure no regressions.
    - Manual verification confirms the separator now only appears after
    actual work is completed.
    
    ---------
    
    Co-authored-by: Josh McKinney <joshka@openai.com>
  • Revert empty paste image handling (#9318)
    Revert #9049 behavior so empty paste events no longer trigger a
    clipboard image read.
  • Support SKILL.toml file. (#9125)
    We’re introducing a new SKILL.toml to hold skill metadata so Codex can
    deliver a richer Skills experience.
    
    Initial focus is the interface block:
    ```
    [interface]
    display_name = "Optional user-facing name"
    short_description = "Optional user-facing description"
    icon_small = "./assets/small-400px.png"
    icon_large = "./assets/large-logo.svg"
    brand_color = "#3B82F6"
    default_prompt = "Optional surrounding prompt to use the skill with"
    ```
    
    All fields are exposed via the app server API.
    display_name and short_description are consumed by the TUI.
  • Revert recent styling change for input prompt placeholder text (#9307)
    A recent change in commit ccba737d26 modified the styling of the
    placeholder text (e.g. "Implement {feature}") in the input box of the
    CLI, changing it from non-italic to italic. I think this was likely
    unintentional. It results in a bad display appearance on some terminal
    emulators, and several users have complained about it.
    
    This change switches back to non-italic styling, restoring the older
    behavior.
    
    It addresses #9262
  • nit: clean unified exec background processes (#9304)
    To fix the occurences where the End event is received after the listener
    stopped listenning
  • Propagate MCP disabled reason (#9207)
    Indicate why MCP servers are disabled when they are disabled by
    requirements:
    
    ```
    ➜  codex git:(main) ✗ just codex mcp list
    cargo run --bin codex -- "$@"
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
         Running `target/debug/codex mcp list`
    Name         Command          Args  Env  Cwd  Status                                                                  Auth
    docs         docs-mcp         -     -    -    disabled: requirements (MDM com.openai.codex:requirements_toml_base64)  Unsupported
    hello_world  hello-world-mcp  -     -    -    disabled: requirements (MDM com.openai.codex:requirements_toml_base64)  Unsupported
    
    ➜  codex git:(main) ✗ just c
    cargo run --bin codex -- "$@"
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.90s
         Running `target/debug/codex`
    ╭─────────────────────────────────────────────╮
    │ >_ OpenAI Codex (v0.0.0)                    │
    │                                             │
    │ model:     gpt-5.2 xhigh   /model to change │
    │ directory: ~/code/codex/codex-rs            │
    ╰─────────────────────────────────────────────╯
    
    /mcp
    
    🔌  MCP Tools
    
      • No MCP tools available.
    
      • docs (disabled)
        • Reason: requirements (MDM com.openai.codex:requirements_toml_base64)
    
      • hello_world (disabled)
        • Reason: requirements (MDM com.openai.codex:requirements_toml_base64)
    ```
  • Changed codex resume --last to honor the current cwd (#9245)
    This PR changes `codex resume --last` to work consistently with `codex
    resume`. Namely, it filters based on the cwd when selecting the last
    session. It also supports the `--all` modifier as an override.
    
    This addresses #8700
  • Add text element metadata to types (#9235)
    Initial type tweaking PR to make the diff of
    https://github.com/openai/codex/pull/9116 smaller
    
    This should not change any behavior, just adds some fields to types
  • add WebSearchMode enum (#9216)
    ### What
    Add `WebSearchMode` enum (disabled, cached live, defaults to cached) to
    config + V2 protocol. This enum takes precedence over legacy flags:
    `web_search_cached`, `web_search_request`, and `tools.web_search`.
    
    Keep `--search` as live.
    
    ### Tests
    Added tests
  • fix(tui): disable double-press quit shortcut (#9220)
    Disables the default Ctrl+C/Ctrl+D double-press quit UX (keeps the code
    path behind a const) while we rethink the quit/interrupt flow.
    
    Tests:
    - just fmt
    - cargo clippy --fix --all-features --tests --allow-dirty --allow-no-vcs
    -p codex-tui
    - cargo test -p codex-tui --lib
  • fix(tui2): align Steer submit keys (#9218)
    - Remove legacy Ctrl+K queuing in tui2; Tab is the queue key.
    - Make Enter queue when Steer is disabled and submit immediately when
    Steer is enabled.
    - Add Steer keybinding docs on both tui and tui2 chat composers.
  • Get model on session configured (#9191)
    - Don't try to precompute model unless you know it from `config`
    - Block `/model` on session configured
    - Queue messages until session configured
    - show "loading" in status until session configured
  • feat: emit events around collab tools (#9095)
    Emit the following events around the collab tools. On the `app-server`
    this will be under `item/started` and `item/completed`
    ```
    #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
    pub struct CollabAgentSpawnBeginEvent {
        /// Identifier for the collab tool call.
        pub call_id: String,
        /// Thread ID of the sender.
        pub sender_thread_id: ThreadId,
        /// Initial prompt sent to the agent. Can be empty to prevent CoT leaking at the
        /// beginning.
        pub prompt: String,
    }
    
    #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
    pub struct CollabAgentSpawnEndEvent {
        /// Identifier for the collab tool call.
        pub call_id: String,
        /// Thread ID of the sender.
        pub sender_thread_id: ThreadId,
        /// Thread ID of the newly spawned agent, if it was created.
        pub new_thread_id: Option<ThreadId>,
        /// Initial prompt sent to the agent. Can be empty to prevent CoT leaking at the
        /// beginning.
        pub prompt: String,
        /// Last known status of the new agent reported to the sender agent.
        pub status: AgentStatus,
    }
    
    #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
    pub struct CollabAgentInteractionBeginEvent {
        /// Identifier for the collab tool call.
        pub call_id: String,
        /// Thread ID of the sender.
        pub sender_thread_id: ThreadId,
        /// Thread ID of the receiver.
        pub receiver_thread_id: ThreadId,
        /// Prompt sent from the sender to the receiver. Can be empty to prevent CoT
        /// leaking at the beginning.
        pub prompt: String,
    }
    
    #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
    pub struct CollabAgentInteractionEndEvent {
        /// Identifier for the collab tool call.
        pub call_id: String,
        /// Thread ID of the sender.
        pub sender_thread_id: ThreadId,
        /// Thread ID of the receiver.
        pub receiver_thread_id: ThreadId,
        /// Prompt sent from the sender to the receiver. Can be empty to prevent CoT
        /// leaking at the beginning.
        pub prompt: String,
        /// Last known status of the receiver agent reported to the sender agent.
        pub status: AgentStatus,
    }
    
    #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
    pub struct CollabWaitingBeginEvent {
        /// Thread ID of the sender.
        pub sender_thread_id: ThreadId,
        /// Thread ID of the receiver.
        pub receiver_thread_id: ThreadId,
        /// ID of the waiting call.
        pub call_id: String,
    }
    
    #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
    pub struct CollabWaitingEndEvent {
        /// Thread ID of the sender.
        pub sender_thread_id: ThreadId,
        /// Thread ID of the receiver.
        pub receiver_thread_id: ThreadId,
        /// ID of the waiting call.
        pub call_id: String,
        /// Last known status of the receiver agent reported to the sender agent.
        pub status: AgentStatus,
    }
    
    #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
    pub struct CollabCloseBeginEvent {
        /// Identifier for the collab tool call.
        pub call_id: String,
        /// Thread ID of the sender.
        pub sender_thread_id: ThreadId,
        /// Thread ID of the receiver.
        pub receiver_thread_id: ThreadId,
    }
    
    #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
    pub struct CollabCloseEndEvent {
        /// Identifier for the collab tool call.
        pub call_id: String,
        /// Thread ID of the sender.
        pub sender_thread_id: ThreadId,
        /// Thread ID of the receiver.
        pub receiver_thread_id: ThreadId,
        /// Last known status of the receiver agent reported to the sender agent before
        /// the close.
        pub status: AgentStatus,
    }
    ```
  • tui: double-press Ctrl+C/Ctrl+D to quit (#8936)
    ## Problem
    
    Codex’s TUI quit behavior has historically been easy to trigger
    accidentally and hard to reason
    about.
    
    - `Ctrl+C`/`Ctrl+D` could terminate the UI immediately, which is a
    common key to press while trying
      to dismiss a modal, cancel a command, or recover from a stuck state.
    - “Quit” and “shutdown” were not consistently separated, so some exit
    paths could bypass the
      shutdown/cleanup work that should run before the process terminates.
    
    This PR makes quitting both safer (harder to do by accident) and more
    uniform across quit
    gestures, while keeping the shutdown-first semantics explicit.
    
    ## Mental model
    
    After this change, the system treats quitting as a UI request that is
    coordinated by the app
    layer.
    
    - The UI requests exit via `AppEvent::Exit(ExitMode)`.
    - `ExitMode::ShutdownFirst` is the normal user path: the app triggers
    `Op::Shutdown`, continues
    rendering while shutdown runs, and only ends the UI loop once shutdown
    has completed.
    - `ExitMode::Immediate` exists as an escape hatch (and as the
    post-shutdown “now actually exit”
    signal); it bypasses cleanup and should not be the default for
    user-triggered quits.
    
    User-facing quit gestures are intentionally “two-step” for safety:
    
    - `Ctrl+C` and `Ctrl+D` no longer exit immediately.
    - The first press arms a 1-second window and shows a footer hint (“ctrl
    + <key> again to quit”).
    - Pressing the same key again within the window requests a
    shutdown-first quit; otherwise the
      hint expires and the next press starts a fresh window.
    
    Key routing remains modal-first:
    
    - A modal/popup gets first chance to consume `Ctrl+C`.
    - If a modal handles `Ctrl+C`, any armed quit shortcut is cleared so
    dismissing a modal cannot
      prime a subsequent `Ctrl+C` to quit.
    - `Ctrl+D` only participates in quitting when the composer is empty and
    no modal/popup is active.
    
    The design doc `docs/exit-confirmation-prompt-design.md` captures the
    intended routing and the
    invariants the UI should maintain.
    
    ## Non-goals
    
    - This does not attempt to redesign modal UX or make modals uniformly
    dismissible via `Ctrl+C`.
    It only ensures modals get priority and that quit arming does not leak
    across modal handling.
    - This does not introduce a persistent confirmation prompt/menu for
    quitting; the goal is to keep
      the exit gesture lightweight and consistent.
    - This does not change the semantics of core shutdown itself; it changes
    how the UI requests and
      sequences it.
    
    ## Tradeoffs
    
    - Quitting via `Ctrl+C`/`Ctrl+D` now requires a deliberate second
    keypress, which adds friction for
      users who relied on the old “instant quit” behavior.
    - The UI now maintains a small time-bounded state machine for the armed
    shortcut, which increases
      complexity and introduces timing-dependent behavior.
    
    This design was chosen over alternatives (a modal confirmation prompt or
    a long-lived “are you
    sure” state) because it provides an explicit safety barrier while
    keeping the flow fast and
    keyboard-native.
    
    ## Architecture
    
    - `ChatWidget` owns the quit-shortcut state machine and decides when a
    quit gesture is allowed
      (idle vs cancellable work, composer state, etc.).
    - `BottomPane` owns rendering and local input routing for modals/popups.
    It is responsible for
    consuming cancellation keys when a view is active and for
    showing/expiring the footer hint.
    - `App` owns shutdown sequencing: translating
    `AppEvent::Exit(ShutdownFirst)` into `Op::Shutdown`
      and only terminating the UI loop when exit is safe.
    
    This keeps “what should happen” decisions (quit vs interrupt vs ignore)
    in the chat/widget layer,
    while keeping “how it looks and which view gets the key” in the
    bottom-pane layer.
    
    ## Observability
    
    You can tell this is working by running the TUIs and exercising the quit
    gestures:
    
    - While idle: pressing `Ctrl+C` (or `Ctrl+D` with an empty composer and
    no modal) shows a footer
    hint for ~1 second; pressing again within that window exits via
    shutdown-first.
    - While streaming/tools/review are active: `Ctrl+C` interrupts work
    rather than quitting.
    - With a modal/popup open: `Ctrl+C` dismisses/handles the modal (if it
    chooses to) and does not
    arm a quit shortcut; a subsequent quick `Ctrl+C` should not quit unless
    the user re-arms it.
    
    Failure modes are visible as:
    
    - Quits that happen immediately (no hint window) from `Ctrl+C`/`Ctrl+D`.
    - Quits that occur while a modal is open and consuming `Ctrl+C`.
    - UI termination before shutdown completes (cleanup skipped).
    
    ## Tests
    
    - Updated/added unit and snapshot coverage in `codex-tui` and
    `codex-tui2` to validate:
      - The quit hint appears and expires on the expected key.
    - Double-press within the window triggers a shutdown-first quit request.
    - Modal-first routing prevents quit bypass and clears any armed shortcut
    when a modal consumes
        `Ctrl+C`.
    
    These tests focus on the UI-level invariants and rendered output; they
    do not attempt to validate
    real terminal key-repeat timing or end-to-end process shutdown behavior.
    
    ---
    Screenshot:
    <img width="912" height="740" alt="Screenshot 2026-01-13 at 1 05 28 PM"
    src="https://github.com/user-attachments/assets/18f3d22e-2557-47f2-a369-ae7a9531f29f"
    />