Commit Graph

285 Commits

  • [connectors] Support connectors part 2 - slash command and tui (#9728)
    - [x] Support `/apps` slash command to browse the apps in tui.
    - [x] Support inserting apps to prompt using `$`.
    - [x] Lots of simplification/renaming from connectors to apps.
  • chore(config) personality as a feature (#10116)
    ## Summary
    Sets up an explicit Feature flag for `/personality`, so users can now
    opt in to it via `/experimental`. #10114 also updates the config
    
    ## Testing
    - [x] Tested locally
  • [skills] Auto install MCP dependencies when running skils with dependency specs. (#9982)
    Auto install MCP dependencies when running skils with dependency specs.
  • Restore image attachments/text elements when recalling input history (Up/Down) (#9628)
    **Summary**
    - Up/Down input history now restores image attachments and text elements
    for local entries.
    - Composer history stores rich local entries (text + text elements +
    local image paths) while persistent history remains text-only.
    - Added tests to verify history recall rehydrates image placeholders and
    attachments in both `tui` and `tui2`.
    
    **Changes**
    - `tui/src/bottom_pane/chat_composer_history.rs`: store `HistoryEntry`
    (text + elements + image paths) for local history; adapt navigation +
    tests.
    - `tui2/src/bottom_pane/chat_composer_history.rs`: same as above.
    - `tui/src/bottom_pane/chat_composer.rs`: record rich history entries
    and restore them on Up/Down; update Ctrl+C history and tests.
    - `tui2/src/bottom_pane/chat_composer.rs`: same as above.
  • remove sandbox globals. (#9797)
    Threads sandbox updates through OverrideTurnContext for active turn
    Passes computed sandbox type into safety/exec
  • TUI footer: right-align context and degrade shortcut summary + mode cleanly (#9944)
    ## Summary
    Refines the bottom footer layout to keep `% context left` right-aligned
    while making the left side degrade cleanly
    
    ## Behavior with empty textarea
    Full width:
    <img width="607" height="62" alt="Screenshot 2026-01-26 at 2 59 59 PM"
    src="https://github.com/user-attachments/assets/854f33b7-d714-40be-8840-a52eb3bda442"
    />
    Less:
    <img width="412" height="66" alt="Screenshot 2026-01-26 at 2 59 48 PM"
    src="https://github.com/user-attachments/assets/9c501788-c3a2-4b34-8f0b-8ec4395b44fe"
    />
    Min width:
    <img width="218" height="77" alt="Screenshot 2026-01-26 at 2 59 33 PM"
    src="https://github.com/user-attachments/assets/0bed2385-bdbf-4254-8ae4-ab3452243628"
    />
    
    ## Behavior with message in textarea and agent running (steer enabled)
    Full width:
    <img width="753" height="63" alt="Screenshot 2026-01-26 at 4 33 54 PM"
    src="https://github.com/user-attachments/assets/1856b352-914a-44cf-813d-1cb50c7f183b"
    />
    
    Less:
    <img width="353" height="61" alt="Screenshot 2026-01-26 at 4 30 12 PM"
    src="https://github.com/user-attachments/assets/d951c4d5-f3e7-4116-8fe1-6a6c712b3d48"
    />
    
    Less:
    <img width="304" height="64" alt="Screenshot 2026-01-26 at 4 30 51 PM"
    src="https://github.com/user-attachments/assets/1433e994-5cbc-4e20-a98a-79eee13c8699"
    />
    
    Less:
    <img width="235" height="61" alt="Screenshot 2026-01-26 at 4 30 56 PM"
    src="https://github.com/user-attachments/assets/e216c3c6-84cd-40fc-ae4d-83bf28947f0e"
    />
    
    Less:
    <img width="165" height="59" alt="Screenshot 2026-01-26 at 4 31 08 PM"
    src="https://github.com/user-attachments/assets/027de5de-7185-47ce-b1cc-5363ea33d9b1"
    />
    
    ## Notes / Edge Cases
    - In steer mode while typing, the queue hint no longer replaces the mode
    label; it renders as `tab to queue message · {Mode}`.
    - Collapse priorities differ by state:
    - With the queue hint active, `% context left` is hidden before
    shortening or dropping the queue hint.
    - In the empty + non-running state, `? for shortcuts` is dropped first,
    and `% context left` is only shown if `(shift+tab to
    cycle)` can also fit.
    - Transient instructional states (`?` overlay, Esc hint, Ctrl+C/D
    reminders, and flash/override hints) intentionally suppress the
    mode label (and context) to focus the next action.
    
    ## Implementation Notes
    - Renamed the base footer modes to make the state explicit:
    `ComposerEmpty` and `ComposerHasDraft`, and compute the base mode
    directly from emptiness.
    - Unified collapse behavior in `single_line_footer_layout` for both base
    modes, with:
    - Queue-hint behavior that prefers keeping the queue hint over context.
    - A cycle-hint guard that prevents context from reappearing after
    `(shift+tab to cycle)` is dropped.
    - Kept rendering responsibilities explicit:
      - `single_line_footer_layout` decides what fits.
      - `render_footer_line` renders a chosen line.
    - `render_footer_from_props` renders the canonical mode-to-text mapping.
    - Expanded snapshot coverage:
    - Added `footer_collapse_snapshots` in `chat_composer.rs` to lock the
    distinct collapse states across widths.
    - Consolidated the width-aware snapshot helper usage (e.g.,
    `snapshot_composer_state_with_width`,
    `snapshot_footer_with_mode_indicator`).
  • Aligned feature stage names with public feature maturity stages (#9929)
    We've recently standardized a [feature maturity
    model](https://developers.openai.com/codex/feature-maturity) that we're
    using in our docs and support forums to communicate expectations to
    users. This PR updates the internal stage names and descriptions to
    match.
    
    This change involves a simple internal rename and updates to a few
    user-visible strings. No functional change.
  • feat(tui) /personality (#9718)
    ## Summary
    Adds /personality selector in the TUI, which leverages the new core
    interface in #9644
    
    Notes:
    - We are doing some of our own state management for model_info loading
    here, but not sure if that's ideal. open to opinions on simpler
    approach, but would like to avoid blocking on a larger refactor
    - Right now, the `/personality` selector just hides when the model
    doesn't support it. we can update this behavior down the line
    
    ## Testing
    - [x] Tested locally
    - [x] Added snapshot tests
  • Use collaboration mode masks without mutating base settings (#9806)
    Keep an unmasked base collaboration mode and apply the active mask on
    demand. Simplify the TUI mask helpers and update tests/docs to match the
    mask contract.
  • feat: ephemeral threads (#9765)
    Add ephemeral threads capabilities. Only exposed through the
    `app-server` v2
    
    The idea is to disable the rollout recorder for those threads.
  • change collaboration mode to struct (#9793)
    Shouldn't cause behavioral change
  • TUI: prompt to implement plan and switch to Execute (#9712)
    ## Summary
    - Replace the plan‑implementation prompt with a standard selection
    popup.
    - “Yes” submits a user turn in Execute via a dedicated app event to
    preserve normal transcript behavior.
    - “No” simply dismisses the popup.
    
    <img width="977" height="433" alt="Screenshot 2026-01-22 at 2 00 54 PM"
    src="https://github.com/user-attachments/assets/91fad06f-7b7a-4cd8-9051-f28a19b750b2"
    />
    
    ## Changes
    - Add a plan‑implementation popup using `SelectionViewParams`.
    - Add `SubmitUserMessageWithMode` so “Yes” routes through
    `submit_user_message` (ensures user history + separator state).
    - Track `saw_plan_update_this_turn` so the prompt appears even when only
    `update_plan` is emitted.
    - Suppress the plan popup on replayed turns, when messages are queued,
    or when a rate‑limit prompt is pending.
    - Add `execute_mode` helper for collaboration modes.
    - Add tests for replay/queued/rate‑limit guards and plan update without
    final message.
    - Add snapshots for both the default and “No”‑selected popup states.
  • feat(core) update Personality on turn (#9644)
    ## Summary
    Support updating Personality mid-Thread via UserTurn/OverwriteTurn. This
    is explicitly unused by the clients so far, to simplify PRs - app-server
    and tui implementations will be follow-ups.
    
    ## Testing
    - [x] added integration tests
  • feat(tui) /permissions flow (#9561)
    ## Summary
    Adds the `/permissions` command, with a (usually) shorter set of
    permissions. `/approvals` still exists, for backwards compatibility.
    
    <img width="863" height="309" alt="Screenshot 2026-01-20 at 4 12 51 PM"
    src="https://github.com/user-attachments/assets/c49b5ba5-bc47-46dd-9067-e1a5670328fe"
    />
    
    
    ## Testing
    - [x] updated unit tests
    - [x] Tested locally
  • Add UI for skill enable/disable. (#9627)
    "/skill" will now allow you to enable/disable skills:
    <img width="658" height="199" alt="image"
    src="https://github.com/user-attachments/assets/bf8994c8-d6c1-462f-8bbb-f1ee9241caa4"
    />
  • fix(tui) turn timing incremental (#9599)
    ## Summary
    When we send multiple assistant messages, reset the timer so "Worked for
    2m 36s" is the time since the last time we showed the message, rather
    than an ever-increasing number.
    
    We could instead change the copy so it's more clearly a running counter.
    
    ## Testing
    - [x] ran locally
    
    <img width="903" height="732" alt="Screenshot 2026-01-21 at 1 42 51 AM"
    src="https://github.com/user-attachments/assets/bb4d827b-3a0e-48ba-bd6a-d8cd65d8e892"
    />
  • define/emit some metrics for windows sandbox setup (#9573)
    This should give us visibility into how users are using the elevated
    sandbox nux flow, and the timing of the elevated setup.
  • Add request-user-input overlay (#9585)
    - Add request-user-input overlay and routing in the TUI
  • 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.
  • 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
  • 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: 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`.
  • 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: /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.
  • 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>
  • nit: clean unified exec background processes (#9304)
    To fix the occurences where the End event is received after the listener
    stopped listenning
  • 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
  • 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
  • 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
  • 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"
    />
  • fix: report an appropriate error in the TUI for malformed rules (#9011)
    The underlying issue is that when we encountered an error starting a
    conversation (any sort of error, though making `$CODEX_HOME/rules` a
    file rather than folder was the example in #8803), then we were writing
    the message to stderr, but this could be printed over by our UI
    framework so the user would not see it. In general, we disallow the use
    of `eprintln!()` in this part of the code for exactly this reason,
    though this was suppressed by an `#[allow(clippy::print_stderr)]`.
    
    This attempts to clean things up by changing `handle_event()` and
    `handle_tui_event()` to return a `Result<AppRunControl>` instead of a
    `Result<bool>`, which is a new type introduced in this PR (and depends
    on `ExitReason`, also a new type):
    
    ```rust
    #[derive(Debug)]
    pub(crate) enum AppRunControl {
        Continue,
        Exit(ExitReason),
    }
    
    #[derive(Debug, Clone)]
    pub enum ExitReason {
        UserRequested,
        Fatal(String),
    }
    ```
    
    This makes it possible to exit the primary control flow of the TUI with
    richer information. This PR adds `ExitReason` to the existing
    `AppExitInfo` struct and updates `handle_app_exit()` to print the error
    and exit code `1` in the event of `ExitReason::Fatal`.
    
    I tried to create an integration test for this, but it was a bit
    involved, so I published it as a separate PR:
    https://github.com/openai/codex/pull/9166. For this PR, please have
    faith in my manual testing!
    
    Fixes https://github.com/openai/codex/issues/8803.
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/9011).
    * #9166
    * __->__ #9011
  • Fix spinner/Esc interrupt when MCP startup completes mid-turn (#8661)
    ## **Problem**
    
    Codex’s TUI uses a single “task running” indicator (spinner + Esc interrupt hint)
    to communicate “the UI is busy”. In practice, “busy” can mean two different
    things: an agent turn is running, or MCP servers are still starting up. Without a
    clear contract, those lifecycles can interfere: startup completion can clear the
    spinner while a turn is still in progress, or the UI can appear idle while MCP is
    still booting. This is user-visible confusion during the most important moments
    (startup and the first turn), so it was worth making the contract explicit and
    guarding it.
    
    ## **Mental model**
    
    `ChatWidget` is the UI-side adapter for the `codex_core::protocol` event stream.
    It receives `EventMsg` events and updates two major UI surfaces: the transcript
    (history/streaming cells) and the bottom pane (composer + status indicator).
    
    The key concept after this change is that the bottom pane’s “task running”
    indicator is treated as **derived UI-busy state**, not “agent is running”. It is
    considered active while either:
    - an agent turn is in progress (`TurnStarted` → completion/abort), or
    - MCP startup is in progress (`McpStartupUpdate` → `McpStartupComplete`).
    
    Those lifecycles are tracked independently, and the bottom-pane indicator is
    defined as their union.
    
    ## **Non-goals**
    
    - This does not introduce separate UI indicators for “turn busy” vs “MCP busy”.
    - This does not change MCP startup behavior, ordering guarantees, or core
      protocol semantics.
    - This does not rework unrelated status/header rendering or transcript layout.
    
    ## **Tradeoffs**
    
    - The “one flag represents multiple lifecycles” approach remains lossy: it
      preserves correct “busy vs idle” semantics but cannot express *which* kind of
      busy is happening without further UI changes.
    - The design keeps complexity low by keeping a single derived boolean, rather
      than adding a more expressive bottom-pane state machine. That’s chosen because
      it matches existing UX and minimizes churn while fixing the confusion.
    
    ## **Architecture**
    
    - `codex-core` owns the actual lifecycles and emits `codex_core::protocol`
      events.
    - `ChatWidget` owns the UI interpretation of those lifecycles. It is responsible
      for keeping the bottom pane’s derived “busy” state consistent with the event
      stream, and for updating the status header when MCP progress updates arrive.
    - The bottom pane remains a dumb renderer of the single “task running” flag; it
      does not learn about MCP or agent turns directly.
    
    ## **Observability**
    
    - When working: the spinner/Esc hint stays visible during MCP startup and does
      not disappear mid-turn when `McpStartupComplete` arrives; startup status
      headers can update without clearing “busy” for an active turn.
    - When broken: you’ll see the spinner/hint flicker off while output is still
      streaming, or the UI appears idle while MCP startup status is still changing.
    
    ## **Tests**
    
    - Adds/strengthens a regression test that asserts MCP startup completion does
      not clear the “task running” indicator for an active turn (in both `tui` and
      `tui2` variants).
    - These tests prove the **contract** (“busy is the union of turn + startup”) at
      the UI boundary; they do not attempt to validate MCP startup ordering,
      real-world startup timing, or backend integration behavior.
    
    Fixes #7017
    
    Signed-off-by: 2mawi2 <2mawi2@users.noreply.github.com>
    Co-authored-by: 2mawi2 <2mawi2@users.noreply.github.com>
    Co-authored-by: Josh McKinney <joshka@openai.com>
  • Fix queued messages during /review (#9122)
    Sending a message during /review interrupts the review, whereas during
    normal operation, sending a message while the agent is running will
    queue the message. This is unexpected behavior, and since /review
    usually takes a while, it takes away a potentially useful operation.
    
    Summary
    - Treat review mode as an active task for message queuing so inputs
    don’t inject into the running review turn.
    - Prevents user submissions from rendering immediately in the transcript
    while the review continues streaming.
    - Keeps review UX consistent with normal “task running” behavior and
    avoids accidental interrupt/replacement.
    
    Notes
    - This change only affects UI queuing logic; core review flow and task
    lifecycle remain unchanged.
  • Show tab queue hint in footer (#9138)
    - show the Tab queue hint in the footer when a task is running with
    Steer enabled
    - drop the history queue hint and add footer snapshots
  • 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.
  • feat: wire fork to codex cli (#8994)
    ## Summary
    - add `codex fork` subcommand and `/fork` slash command mirroring resume
    - extend session picker to support fork/resume actions with dynamic
    labels in tui/tui2
    - wire fork selection flow through tui bootstraps and add fork-related
    tests
  • Label attached images so agent can understand in-message labels (#8950)
    Agent wouldn't "see" attached images and would instead try to use the
    view_file tool:
    <img width="1516" height="504" alt="image"
    src="https://github.com/user-attachments/assets/68a705bb-f962-4fc1-9087-e932a6859b12"
    />
    
    In this PR, we wrap image content items in XML tags with the name of
    each image (now just a numbered name like `[Image #1]`), so that the
    model can understand inline image references (based on name). We also
    put the image content items above the user message which the model seems
    to prefer (maybe it's more used to definitions being before references).
    
    We also tweak the view_file tool description which seemed to help a bit
    
    Results on a simple eval set of images:
    
    Before
    <img width="980" height="310" alt="image"
    src="https://github.com/user-attachments/assets/ba838651-2565-4684-a12e-81a36641bf86"
    />
    
    After
    <img width="918" height="322" alt="image"
    src="https://github.com/user-attachments/assets/10a81951-7ee6-415e-a27e-e7a3fd0aee6f"
    />
    
    ```json
    [
      {
        "id": "single_describe",
        "prompt": "Describe the attached image in one sentence.",
        "images": ["image_a.png"]
      },
      {
        "id": "single_color",
        "prompt": "What is the dominant color in the image? Answer with a single color word.",
        "images": ["image_b.png"]
      },
      {
        "id": "orientation_check",
        "prompt": "Is the image portrait or landscape? Answer in one sentence.",
        "images": ["image_c.png"]
      },
      {
        "id": "detail_request",
        "prompt": "Look closely at the image and call out any small details you notice.",
        "images": ["image_d.png"]
      },
      {
        "id": "two_images_compare",
        "prompt": "I attached two images. Are they the same or different? Briefly explain.",
        "images": ["image_a.png", "image_b.png"]
      },
      {
        "id": "two_images_captions",
        "prompt": "Provide a short caption for each image (Image 1, Image 2).",
        "images": ["image_c.png", "image_d.png"]
      },
      {
        "id": "multi_image_rank",
        "prompt": "Rank the attached images from most colorful to least colorful.",
        "images": ["image_a.png", "image_b.png", "image_c.png"]
      },
      {
        "id": "multi_image_choice",
        "prompt": "Which image looks more vibrant? Answer with 'Image 1' or 'Image 2'.",
        "images": ["image_b.png", "image_d.png"]
      }
    ]
    ```