Commit Graph

407 Commits

  • Rename approvals reviewer variant to auto-review (#19056)
    ## Why
    
    `approvals_reviewer` now uses `auto_review` as the canonical config/API
    value after #18504, but the Rust enum variant and nearby helper/test
    names still used `GuardianSubagent` / guardian approval wording. That
    made follow-up code and reviews confusing even though the external value
    had already moved to Auto-review.
    
    ## What changed
    
    - Renamed `ApprovalsReviewer::GuardianSubagent` to
    `ApprovalsReviewer::AutoReview`.
    - Updated protocol, app-server, config, core, TUI, exec, and analytics
    test callsites.
    - Renamed nearby helper/test names from guardian approval wording to
    Auto-review wording where they refer to the approvals reviewer mode.
    - Preserved wire compatibility:
      - `auto_review` remains the canonical serialized value.
      - `guardian_subagent` remains accepted as a legacy alias.
    
    This intentionally does not rename the `[features].guardian_approval`
    key, `Feature::GuardianApproval`, `core/src/guardian`, analytics event
    names, or app-server Guardian review event types.
    
    ## Verification
    
    - `cargo test -p codex-protocol
    approvals_reviewer_serializes_auto_review_and_accepts_legacy_guardian_subagent`
    - `cargo test -p codex-app-server-protocol
    approvals_reviewer_serializes_auto_review_and_accepts_legacy_guardian_subagent`
    - `cargo test -p codex-config approvals_reviewer`
    - `cargo test -p codex-tui update_feature_flags`
    - `cargo test -p codex-core permissions_instructions`
    - `cargo test -p codex-tui permissions_selection`
  • Move TUI app tests to modules they cover (#18799)
    ## Summary
    
    The TUI app refactor in #18753 moved the old `app.rs` tests into a
    single `app/tests.rs` file. That kept the split mechanically simple, but
    it left several focused unit tests far from the modules they exercise.
    
    This PR is a follow-up that moves tests next to the code they cover.
    
    It also adds `tui/src/app/test_support.rs` for shared fixture
    construction.
    
    This is just a mechanical refactoring (no functional changes) and does
    not affect any production code.
  • Refactor TUI app module into submodules (#18753)
    ## Why
    
    The TUI app module had grown past the 512K source-file cap enforced by
    CI/CD. This keeps the app entry point below that limit while preserving
    the existing runtime behavior and test surface.
    
    ## What changed
    
    - Kept the top-level `App` state and run-loop wiring in
    `tui/src/app.rs`.
    - Split app responsibilities into focused private submodules under
    `tui/src/app/`, covering event dispatch, thread routing, session
    lifecycle, config persistence, background requests, startup prompts,
    input, history UI, platform actions, and thread event buffering.
    - Moved the existing app-level tests into `tui/src/app/tests.rs` and
    reused the existing snapshot location rather than adding new tests or
    snapshots.
    - Added module header comments for `app.rs` and the new submodules.
    
    ## Follow-up
    
    A future cleanup can move narrow unit tests from `tui/src/app/tests.rs`
    into the specific app submodules they exercise. This PR keeps the
    existing app-level tests together so the refactor stays focused on the
    source-file split.
    
    ## Verification
    
    - `cargo test -p codex-tui --lib
    app::tests::agent_picker_item_name_snapshot`
    - `cargo test -p codex-tui --lib app::tests::clear_ui`
    - `cargo test -p codex-tui --lib
    app::tests::ctrl_l_clear_ui_after_long_transcript_reuses_clear_header_snapshot`
    - `just fix -p codex-tui`
    
    Full `cargo test -p codex-tui` still fails on model-catalog drift
    unrelated to this refactor, including stale
    `gpt-5.3-codex`/`gpt-5.1-codex` snapshot and migration expectations now
    resolving to `gpt-5.4`.
  • Use app server metadata for fork parent titles (#18632)
    ## Problem
    The TUI resolved fork parent titles from local CODEX_HOME metadata,
    which could show missing or stale titles when app-server metadata is
    authoritative.
    
    This is a lingering bug left over from the migration of the TUI to the
    app-server interface. I found it when I asked Codex to review all places
    where the TUI code was still directly accessing the local CODEX_HOME.
    
    ## Solution
    Route fork parent title metadata through the app-server session state
    and render only that supplied title, with focused snapshot coverage for
    stale local metadata.
    
    ## Testing
    I manually tested by renaming a thread then forking it and confirming
    that the "forked from" message indicated the parent thread's name.
  • Fix stale model test fixtures (#18719)
    Fixes stale test fixtures left after the active bundled model catalog
    updates in #18586 and #18388. Those changes made `gpt-5.4` the current
    default and removed several older hardcoded slugs, which left Windows
    Bazel shards failing TUI and config tests.
    
    What changed:
    - Refresh TUI model migration, availability NUX, plan-mode, status, and
    snapshot fixtures to use active bundled model slugs.
    - Update the config edit test expectation for the TOML-quoted
    `"gpt-5.2"` migration key.
    - Move the model catalog tests into
    `codex-rs/tui/src/app/tests/model_catalog.rs` so touching them does not
    trip the blob-size policy for `app.rs`.
    
    Verification:
    - CI Bazel/lint checks are expected to cover the affected test shards.
  • Surface TUI skills refresh failures (#18627)
    ## Why
    
    `skills/list` refreshes are best-effort metadata updates. If one fails
    during startup or thread switching, the TUI should keep running and show
    enough detail to diagnose the app-server failure instead of leaving the
    user with only a log entry.
    
    This addresses the recoverability and observability issue reported in
    #16914.
    
    ## What Changed
    
    - Preserve the full startup `skills/list` error chain before sending it
    back through the app event queue.
    - Surface failed skills refreshes as recoverable TUI error messages
    while still logging the warning.
    
    This is related to the recent bug fix from [PR
    #18370](https://github.com/openai/codex/pull/18370).
  • Remove simple TUI legacy_core reexports (#18631)
    ## Problem
    The TUI still imported path utilities and config-loader symbols through
    app-server-client's legacy_core facade even though those APIs already
    exist in utility/config crates. This is part of our ongoing effort to
    whittle away at these old dependencies.
    
    ## Solution
    Rewire imports to avoid the TUI directly importing from the core crate
    and instead import from common lower-level crates. This PR doesn't
    include any functional changes; it's just a simple rewiring.
  • protocol: canonicalize file system permissions (#18274)
    ## Why
    
    `PermissionProfile` needs stable, canonical file-system semantics before
    it can become the primary runtime permissions abstraction. Without a
    canonical form, callers have to keep re-deriving legacy sandbox maps and
    profile comparisons remain lossy or order-dependent.
    
    ## What changed
    
    This adds canonicalization helpers for `FileSystemPermissions` and
    `PermissionProfile`, expands special paths into explicit sandbox
    entries, and updates permission request/conversion paths to consume
    those canonical entries. It also tightens the legacy bridge so root-wide
    write profiles with narrower carveouts are not silently projected as
    full-disk legacy access.
    
    ## Verification
    
    - `cargo test -p codex-protocol
    root_write_with_read_only_child_is_not_full_disk_write -- --nocapture`
    - `cargo test -p codex-sandboxing permission -- --nocapture`
    - `cargo test -p codex-tui permissions -- --nocapture`
  • Surface parent thread status in side conversations (#18591)
    ## Summary
    
    Side conversations can hide important state changes from the parent
    conversation while the user is focused on the side thread. In
    particular, the parent may finish, fail, need user input, or require an
    approval while the side conversation remains visible. Users need a
    lightweight signal for those states, but parent approval overlays should
    not interrupt the side conversation itself.
    
    This change adds parent-conversation status to the side conversation
    context label and defers parent interactive overlays while side mode is
    active. When the user exits side mode, pending parent approvals and
    input requests are restored in the main thread. The pending approval
    footer avoids duplicating the same parent approval status, and replayed
    notice cells are filtered when restoring a pending interactive request
    so tips or warnings do not crowd out the approval prompt.
    
    The change is contained to the TUI side-conversation and thread replay
    paths.
    
    Example 1: Approval pending
    <img width="752" height="35" alt="Screenshot 2026-04-19 at 12 56 07 PM"
    src="https://github.com/user-attachments/assets/1cc0f1a3-9cab-4d60-aed2-96523ccafc20"
    />
    
    Example 2: Turn complete
    <img width="754" height="35" alt="Screenshot 2026-04-19 at 12 56 27 PM"
    src="https://github.com/user-attachments/assets/653521a5-e298-4366-ae1c-72b56eb88eeb"
    />
  • Use app server thread names in TUI picker (#18633)
    ## Problem
    
    The TUI resume/fork picker was backfilling thread names from local
    rollout indexes. This was left over from before the TUI was moved to the
    app server. It should be using app-server APIs because the TUI might be
    connected to a remote connection.
    
    This bug wasn't (yet) reported by a user. I found it by asking Codex to
    review places in the TUI code where it was still directly accessing the
    CODEX_HOME directory rather than going through app-server APIs.
    
    ## Solution
    
    The resume picker and session lookups should use app-server thread APIs
    only. Remove legacy rollout name/list backfills, and avoid local name
    reads in fork history.
    
    ## Testing
    
    I manually tested `codex resume` and `codex resume --all` to look for
    functional or performance regressions in the resume picker.
  • Add verbose diagnostics for /mcp (#18610)
    Fixes #18539.
    
    ## Summary
    The recent `/mcp` performance work kept the default command fast by
    avoiding resource and resource-template inventory probes, but it also
    removed useful diagnostics for users trying to confirm MCP server state.
    
    This keeps bare `/mcp` on the fast tools/auth path and adds `/mcp
    verbose` for the slower diagnostic view. Verbose mode requests full MCP
    server status from the app-server and restores status, resources, and
    resource templates in the TUI output.
    
    ## Testing
    In addition to running automation, I manually tested the feature to
    confirm that it works.
  • [codex] Add workspace owner usage nudge UI (#18221)
    ## Summary
    
    Third PR in the split from #17956. Stacked on #18220.
    
    - shows workspace-owner/member-specific rate-limit messages behind
    `workspace_owner_usage_nudge`
    - prompts workspace members to notify the owner or request a usage-limit
    increase
    - sends the confirmed nudge through the app-server API and renders
    completion feedback
    - adds focused TUI snapshot coverage for prompts and completion states
    - feature gate
    
    ## Validation
    
    - `cargo test -p codex-backend-client`
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-app-server rate_limits`
    - `cargo test -p codex-tui workspace_`
    - `cargo test -p codex-tui status_`
    - `just fmt`
    - `just fix -p codex-backend-client`
    - `just fix -p codex-app-server-protocol`
    - `just fix -p codex-app-server`
    - `just fix -p codex-tui`
  • Use thread IDs in TUI resume hints (#18440)
    ## Summary
    
    Fixes #18313.
    
    Recent TUI resume breadcrumbs could print a thread title instead of the
    stable thread UUID. For sessions whose title was auto-derived from the
    first prompt, that made the suggested codex resume command look like it
    should resume a long prompt rather than the session ID.
    
    This updates the TUI and CLI post-exit resume hints, plus the in-session
    summary shown when switching/forking threads, to always use the stable
    thread ID for these recovery breadcrumbs. Explicit name-based resume
    support remains available elsewhere.
  • Avoid redundant memory enable notice (#18580)
    ## Summary
    
    Fixes #18554.
    
    The `/experimental` menu can submit the full experimental feature state
    even when the user presses Enter without toggling anything. Previously,
    Codex showed `Memories will be enabled in the next session.` whenever
    the submitted updates included `Feature::MemoryTool = true`, so sessions
    where Memories were already enabled could show a redundant warning on a
    no-op save.
    
    This change records whether `Feature::MemoryTool` was enabled before
    applying feature updates and only emits the next-session notice when
    Memories actually transitions from disabled to enabled.
  • Add /side conversations (#18190)
    The TUI supports long-running turns and agent threads, but quick side
    questions have required interrupting the main flow or manually
    forking/navigating threads. This PR adds a guarded `/side` flow so users
    can ask brief side-conversation questions in an ephemeral fork while
    keeping the primary thread focused. This also helps address the feature
    request in #18125.
    
    The implementation creates one side conversation at a time, lets `/side`
    open either an empty side thread or immediately submit `/side
    <question>`, and returns to the parent with Esc or Ctrl+C. Side
    conversations get hidden developer guardrails that treat inherited
    history as reference-only and steer the model away from workspace
    mutations unless explicitly requested in the side conversation.
    
    The TUI hides most slash commands while side mode is active, leaving
    only `/copy`, `/diff`, `/mention`, and `/status` available there.
  • [TUI] add external config migration prompt when start TUI (#17891)
    - add a TUI startup migration prompt for external agent config
    - support migrating external configs including config, skills, AGENTS.md
    and plugins
    - gate the prompt behind features.external_migrate (default false)
    
    <img width="1037" height="480" alt="Screenshot 2026-04-14 at 9 29 14 PM"
    src="https://github.com/user-attachments/assets/6060849b-03cb-429a-9c13-c7bb46ad2e65"
    />
    <img width="713" height="183" alt="Screenshot 2026-04-14 at 9 29 26 PM"
    src="https://github.com/user-attachments/assets/d13f177e-d4c4-479c-8736-ef29636081e1"
    />
    
    ---------
    
    Co-authored-by: Eric Traut <etraut@openai.com>
  • fix: trust-gate project hooks and exec policies (#14718)
    ## Summary
    - trust-gate project `.codex` layers consistently, including repos that
    have `.codex/hooks.json` or `.codex/execpolicy/*.rules` but no
    `.codex/config.toml`
    - keep disabled project layers in the config stack so nested trusted
    project layers still resolve correctly, while preventing hooks and exec
    policies from loading until the project is trusted
    - update app-server/TUI onboarding copy to make the trust boundary
    explicit and add regressions for loader, hooks, exec-policy, and
    onboarding coverage
    
    ## Security
    Before this change, an untrusted repo could auto-load project hooks or
    exec policies from `.codex/` as long as `config.toml` was absent. This
    makes trust the single gate for project-local config, hooks, and exec
    policies.
    
    ## Stack
    - Parent of #15936
    
    ## Test
    - cargo test -p codex-core without_config_toml
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • /plugins: Add inline enablement toggles (#18395)
    This PR adds inline enable/disable controls to the new /plugins browse
    menu. Installed plugins can now be toggled directly from the list with
    keyboard interaction, and the associated config-write plumbing is
    included so the UI and persisted plugin state stay in sync. This also
    includes the queued-write handling needed to avoid stale toggle
    completions overwriting newer intent.
    
    - Add toggleable plugin rows for installed plugins in /plugins
    - Support Space to enable or disable without leaving the list
    - Persist plugin enablement through the existing app/config write path
    - Preserve the current selection while the list refreshes after a toggle
    - Add tests and snapshot updates for toggling behavior
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • feat: Add remote plugin fields to plugin API (#17277)
    ## Summary
    Update the plugin API for the new remote plugin model.
    
    The mental model is no longer “keep local plugin state in sync with
    remote.” Instead, local and remote plugins are becoming separate
    sources. Remote catalog entries can be shown directly from the remote
    API before installation; after installation they are still downloaded
    into the local cache for execution, but remote installed state will come
    from the API and be held in memory rather than being read from config.
    
    • ## API changes
    - Remove `forceRemoteSync` from `plugin/list`, `plugin/install`, and
    `plugin/uninstall`.
      - Remove `remoteSyncError` from `plugin/list`.
      - Add remote-capable metadata to `plugin/list` / `plugin/read`:
        - nullable `marketplaces[].path`
        - `source: { type: "remote", downloadUrl }`
        - URL asset fields alongside local path fields:
      `composerIconUrl`, `logoUrl`, `screenshotUrls`
      - Make `plugin/read` and `plugin/install` source-compatible:
        - `marketplacePath?: AbsolutePathBuf | null`
        - `remoteMarketplaceName?: string | null`
        - exactly one source is required at runtime
  • perf(tui): defer startup skills refresh (#18370)
    # Summary
    
    This removes startup `skills/list` from the critical path to first
    input. In release measurements, median startup-to-input time improved
    from `307.5 ms` to `191.0 ms` across 30 measured runs with 5 warmups.
    
    # Background
    
    Startup currently waits for a forced `skills/list` app-server request
    before scheduling the first usable TUI frame. That makes skill metadata
    freshness part of the process-launch-to-input path, even though the
    prompt can safely accept normal input before skill metadata has finished
    loading.
    
    I measured startup from process launch until the TUI reports that the
    user can type. The measurement harness watched the startup measurement
    record, killed Codex after a successful sample, and enforced a timeout
    so repeated runs would not leave TUI processes behind. The debug runs
    had enough outliers that I used median as the primary signal and ran a
    baseline self-compare to understand the noise floor.
    
    # Why skills/list
    
    The `skills/list` cut was the best practical optimization because it
    improved startup without changing the important readiness contract: when
    the prompt is shown, it is still backed by an active session. Only
    enrichment data arrives later.
    
    | Candidate | Result | Decision |
    | --- | --- | --- |
    | Defer startup `skills/list` | Debug median improved from `524.0 ms` to
    `348.0 ms`; release median improved from `307.5 ms` to `191.0 ms`. |
    Keep |
    | Defer fresh `thread/start` | Debug median improved from `494.0 ms` to
    `256.0 ms`, but the prompt could appear before an active thread was
    attached. | Reject as too risky for this PR |
    | Avoid forced skills config reload | Debug median moved from `509.0 ms`
    to `512.0 ms`. | Reject as neutral |
    | Skip fresh history metadata | Debug median moved from `496.5 ms` to
    `531.5 ms`. | Reject as regression/noise |
    | Defer app-server startup | Not implemented because it would only
    permit a loading frame unless the TUI gained a deliberate pre-server
    state. | Out of scope |
    
    # Implementation
    
    `App::refresh_startup_skills` now clones the app-server request handle,
    spawns a background task, and issues the same forced `skills/list`
    request after the first frame is scheduled. When the request completes,
    the task sends `AppEvent::SkillsListLoaded` back through the normal app
    event queue.
    
    The existing skills response handling still converts the app-server
    response, updates the chat widget, and emits invalid `SKILL.md`
    warnings. Explicit user-initiated skills refreshes still use the
    existing synchronous app command path, so callers that intentionally
    requested fresh skill state do not race ahead of their own refresh.
    
    # Tradeoffs
    
    The main tradeoff is a narrow theoretical race at startup: skill mention
    completion depends on a background `skills/list` response, so it could
    briefly show stale or empty metadata if opened before that response
    arrives. In manual testing, pressing `$` as soon as possible after
    launch still showed populated skill metadata, so this risk appears
    minimal in normal use. Plain input remains available immediately, and
    the UI updates through the existing skills response path once the
    refresh completes.
    
    This PR does not change how skills are discovered, cached,
    force-reloaded, displayed, enabled, or warned about. It only changes
    when the startup refresh is allowed to complete relative to the first
    usable TUI frame.
    
    # Verification
    
    - `cargo test -p codex-tui`
  • feat(tui): add clear-context plan implementation (#17499)
    ## TL;DR
    
    - Adds a second Plan Mode handoff: implement the approved plan after
    clearing context.
    - Keeps the existing same-thread `Yes, implement this plan` action
    unchanged.
    - Reuses the `/clear` thread-start path and submits the approved plan as
    the fresh thread's first prompt.
    - Covers the new popup option, event plumbing, initial-message behavior,
    and disabled states in TUI tests.
    
    ## Problem
    
    Plan Mode already asks whether to implement an approved plan, but the
    only affirmative path continues in the same thread. That is useful when
    the planning conversation itself is still valuable, but it does not
    support the workflow where exploratory planning context is discarded and
    implementation starts from the final approved plan as the only
    model-visible handoff.
    
    <img width="1253" height="869" alt="image"
    src="https://github.com/user-attachments/assets/90023d75-c330-4919-bed8-518671c3474b"
    />
    
    ## Mental model
    
    There are now two implementation choices after a proposed plan. The
    existing choice, `Yes, implement this plan`, is unchanged: it switches
    to Default mode and submits `Implement the plan.` in the current thread.
    The new choice, `Yes, clear context and implement`, treats the proposed
    plan as a handoff artifact. It clears the UI/session context through the
    same thread-start source used by `/clear`, then submits an initial
    prompt containing the approved plan after the fresh thread is
    configured.
    
    The important distinction is that the new path is not compaction. The
    model receives a deliberate implementation prompt built from the
    approved plan markdown, not a summary of the previous planning
    transcript. Both implementation choices require the Default
    collaboration preset to be available, so the popup does not offer a
    coding handoff when the fresh thread would fall back to another mode.
    
    ## Non-goals
    
    This change does not alter `/clear`, `/compact`, or the existing
    same-context Plan Mode implementation option. It does not add protocol
    surface area or app-server schema changes. It also does not carry the
    previous transcript path or a generated planning summary into the new
    model context.
    
    ## Tradeoffs
    
    The fresh-context option relies on the approved plan being sufficiently
    complete. That matches the Plan Mode contract, but it means vague plans
    will produce weaker implementation starts than a compacted transcript
    would. The upside is that rejected ideas, exploratory dead ends, and
    planning corrections do not leak into the implementation turn.
    
    The current implementation stores the latest proposed plan in
    `ChatWidget` rather than deriving it from history cells at selection
    time. This keeps the popup action simple and deterministic, but it makes
    the cache lifecycle important: it must be reset when a new task starts
    so an old plan cannot be submitted later.
    
    ## Architecture
    
    The TUI stores the most recent completed proposed-plan markdown when a
    plan item completes. The Plan Mode approval popup uses that cache to
    enable the fresh-context option and to build a first-turn prompt that
    instructs the model to implement the approved plan in a fresh context.
    
    Selecting the new option emits a TUI-internal
    `ClearUiAndSubmitUserMessage` event. `App` handles that event by reusing
    the existing clear flow: clear terminal state, reset app UI state, start
    a new app-server thread with `ThreadStartSource::Clear`, and attach a
    replacement `ChatWidget` with an initial user message. The existing
    initial-message suppression in `enqueue_primary_thread_session` ensures
    the prompt is submitted only after the new session is configured and any
    startup replay is rendered.
    
    ## Observability
    
    The previous thread remains resumable through the existing clear-session
    summary hint. There is no new telemetry or protocol event for this path,
    so debugging should start at the TUI event boundary: confirm the popup
    emitted `ClearUiAndSubmitUserMessage`, confirm the app-server thread
    start used `ThreadStartSource::Clear`, then confirm the fresh widget
    submitted the initial user message after `SessionConfigured`.
    
    ## Tests
    
    The Plan Mode popup snapshots cover the new option and preserve the
    original option as the first/default action. Unit coverage verifies the
    original same-context option still emits `SubmitUserMessageWithMode`,
    the new option emits `ClearUiAndSubmitUserMessage` with the approved
    plan embedded verbatim, and the clear-context option is disabled when
    Default mode is unavailable or no approved plan exists. The broader
    `codex-tui` test package passes with the updated fresh-thread
    initial-message plumbing.
  • feat: config aliases (#18140)
    Rename `no_memories_if_mcp_or_web_search` →
    `disable_on_external_context` with backward compatibility
    
    While doing so, we add a key alias system on our layer merging system.
    What we try to avoid is a case where a company managed config use an old
    name while the user has a new name in it's local config (which would
    make the deserialization fail)
  • Guardian -> Auto-Review (#18021)
    This PR is a user-facing change for our rebranding of guardian to
    auto-review.
  • [codex] Revoke ChatGPT tokens on logout (#17825)
    ## Summary
    
    This changes Codex logout so managed ChatGPT auth is revoked against
    AuthAPI before local auth state is removed. CLI logout, TUI `/logout`,
    and the app-server account logout path now use the token-revoking logout
    flow instead of only deleting `auth.json` / credential store state.
    
    ## Root Cause
    
    Logout previously cleared only local auth storage. That removed Codex's
    local credentials but did not ask the backend to invalidate the
    refresh/access token state associated with a managed ChatGPT login.
    
    ## Behavior
    
    For managed ChatGPT auth, logout sends the stored refresh token to
    `https://auth.openai.com/oauth/revoke` with `token_type_hint:
    refresh_token` and the Codex OAuth client id, then deletes all local
    auth stores after revocation succeeds. If only an access token is
    available, it falls back to revoking that access token. API key auth and
    externally supplied `chatgptAuthTokens` are still only cleared locally
    because Codex does not own a refresh token for those modes.
    
    Revocation failures are fail-closed: if Codex cannot load stored auth or
    the backend revoke call fails, logout returns an error and leaves local
    auth in place so the user can retry instead of silently clearing local
    state while backend tokens remain valid.
    
    ## Validation
    ran local version of `codex-cli` with staging overrides/harness for auth
    
    ran `codex login` then `codex logout`:
    
    saw auth.json clear and  backend revocation endpoints were called
    
    ```
    POST /oauth/revoke
    status: 200
    
    revoking access token
    should clear auth session
    clearing auth session due to token revocation
    successfully revoked session and access token
    CANONICAL-API-LINE Response: status='200' method='POST' path='/oauth/revoke
    ```
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Add codex_hook_run analytics event (#17996)
    # Why
    Add product analytics for hook handler executions so we can understand
    which hooks are running, where they came from, and whether they
    completed, failed, stopped, or blocked work.
    
    # What
    - add the new `codex_hook_run` analytics event and payload plumbing in
    `codex-rs/analytics`
    - emit hook-run analytics from the shared hook completion path in
    `codex-rs/core`
    - classify hook source from the loaded hook path as `system`, `user`,
    `project`, or `unknown`
    
    ```
    {
      "event_type": "codex_hook_run",
      "event_params": {
        "thread_id": "string",
        "turn_id": "string",
        "model_slug": "string",
        "hook_name": "string, // any HookEventName
        "hook_source": "system | user | project | unknown",
        "status": "completed | failed | stopped | blocked"
      }
    }
    ```
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • feat(permissions): add glob deny-read policy support (#15979)
    ## Summary
    - adds first-class filesystem policy entries for deny-read glob patterns
    - parses config such as :project_roots { "**/*.env" = "none" } into
    pattern entries
    - enforces deny-read patterns in direct read/list helpers
    - fails closed for sandbox execution until platform backends enforce
    glob patterns in #18096
    - preserves split filesystem policy in turn context only when it cannot
    be reconstructed from legacy sandbox policy
    
    ## Stack
    1. This PR - glob deny-read policy/config/direct-tool support
    2. #18096 - macOS and Linux sandbox enforcement
    3. #17740 - managed deny-read requirements
    
    ## Verification
    - just fmt
    - cargo check -p codex-core -p codex-sandboxing --tests
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Avoid fatal TUI errors on skills list failure (#18061)
    Addresses #17951
    
    Problem: The TUI treated skills/list failures as fatal during refresh,
    so proxy/firewall responses that break plugin discovery could crash the
    session.
    
    Solution: Route startup and refresh skills/list responses through shared
    graceful handling that logs a warning and keeps the TUI running.
  • Fix invalid TUI resume hints (#18059)
    Addresses #18011
    
    Problem: #16987 allowed zero-token TUI exits to print resume hints,
    which exposed precomputed thread ids before their rollout files were
    persisted; #17222 made the same invalid hint visible when switching
    sessions via `/resume`.
    
    Solution: Only include resume commands for TUI sessions backed by a
    materialized non-empty rollout, and cover both missing-rollout and
    persisted-rollout summary behavior.
    
    Testing: Manually verified by pressing Ctrl+D before the first prompt
    and confirming that no "to continue this session" message was generated.
  • Fix MCP startup cancellation through app server (#18078)
    Addresses https://github.com/openai/codex/issues/17143
    
    Problem: TUI interrupts without an active turn stopped cancelling slow
    MCP startup after routing through the app-server APIs.
    
    Solution: Route no-active-turn interrupts through app-server as startup
    cancels, acknowledge them immediately, and emit cancelled MCP startup
    updates.
    
    Testing: I manually confirmed that MCP cancellation didn't work prior to
    this PR and works after the fix was in place.
  • Display YOLO mode permissions if set when launching TUI (#17877)
    - When launching the TUI client, if YOLO mode is enabled, display this
    in the header.
    - Eligibility is determined by `approval_policy = "never"` and
    `sandbox_mode = "danger-full-access"`
    
    <img width="886" height="230" alt="image"
    src="https://github.com/user-attachments/assets/d7064778-e32c-4123-8e44-ca0c9016ab09"
    />
  • Dismiss stale app-server requests after remote resolution (#15134)
    Dismiss stale TUI app-server approvals after remote resolution
    
    When an approval, user-input prompt, or elicitation request is resolved
    by another client, the TUI now dismisses the matching local UI instead
    of leaving stale prompts behind and emitting a misleading local
    cancellation.
    
    This change teaches pending app-server request tracking to map
    `serverRequest/resolved` notifications back to the concrete request type
    and stable request key, then propagates that resolved request into TUI
    prompt state. Approval, request-user-input, and MCP elicitation overlays
    now drop the resolved current or queued request quietly, advance to the
    next queued request when present, and avoid emitting abort/cancel events
    for stale UI.
    
    The latest update also retires matching prompts while they are still
    deferred behind active streaming and suppresses buffered active-thread
    requests whose app-server request id has already been resolved before
    drain. `ChatWidget` removes a resolved request from both the deferred
    interrupt queue and the materialized bottom-pane stack, while
    active-thread request handling verifies the app-server request is still
    pending before showing a prompt. Lifecycle events such as exec begin/end
    remain queued so approved work can still render normally.
    
    Tests cover resolved-request mapping, overlay dismissal behavior,
    deferred prompt pruning for same-turn user input, exec approval IDs,
    lifecycle-event retention, and the buffered active-thread ordering
    regression.
    
    Validation:
    - `just fmt`
    - `git diff --check`
    - `cargo test -p codex-tui
    resolved_buffered_approval_does_not_become_actionable_after_drain`
    - `cargo test -p codex-tui
    enqueue_primary_thread_session_replays_buffered_approval_after_attach`
    - `cargo test -p codex-tui chatwidget::interrupts`
    - `just fix -p codex-tui`
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • chore(tui) cleanup (#17920)
    ## Summary
    Cleanup extraneous plugins.
  • feat: reset memories button (#17937)
    <img width="720" height="175" alt="Screenshot 2026-04-15 at 14 35 02"
    src="https://github.com/user-attachments/assets/041d73ff-8c16-42a9-8e92-c245805084f0"
    />
  • feat: memories menu (#17632)
    Add menu that:
    1. If memories feature is not enabled, propose to enable it
    2. Let you choose if you want to generate memories and to use memories
  • Spread AbsolutePathBuf (#17792)
    Mechanical change to promote absolute paths through code.
  • Fix remote skill popup loading (#17702)
    ## Summary
    
    Fix the TUI `$` skill popup so personal skills appear reliably when
    Codex is connected to a remote app-server.
    
    ## What changed
    
    - load skills on TUI startup with an explicit forced refresh
    - refresh skills using the actual current cwd instead of an empty `cwds`
    list
    - resync an already-open `$` popup when skill mentions are updated
    - add a regression test for refreshing an open mention popup
    
    ## Root cause
    
    The TUI was sometimes sending `list_skills` with `cwds: []` after
    `SessionConfigured`.
    
    For the launchd app-server flow, the server resolved that empty cwd list
    to its own process cwd, which was `/`. The response therefore came back
    tagged with `cwd: "/"`, and the TUI later filtered skills by exact cwd
    match against the actual project cwd such as `/Users/starr/code/dream`.
    That dropped all personal skills from the mention list, so `$` only
    showed plugins/apps.
    
    ## Verification
    
    Built successfully with remote cache disabled:
    
    ```bash
    cd /Users/starr/code/codex-worktrees/starr-skill-popup-20260413130509
    bazel --output_base=/tmp/codex-bazel-verify-starr-skill-popup build //codex-rs/cli:codex --noremote_accept_cached --noremote_upload_local_results --disk_cache=
    ```
    
    Also verified interactively in a PTY against the live app-server at
    `ws://127.0.0.1:4511`:
    - launched the built TUI
    - typed `$`
    - confirmed personal skills appeared in the popup, including entries
    such as `Applied Devbox`, `CI Debug`, `Channel Summarization`, `Codex PR
    Review`, and `Daily Digest`
    
    ## Files changed
    
    - `codex-rs/tui/src/app.rs`
    - `codex-rs/tui/src/chatwidget.rs`
    - `codex-rs/tui/src/bottom_pane/chat_composer.rs`
    
    Co-authored-by: Codex <noreply@openai.com>
  • Refactor plugin loading to async (#17747)
    Simplifies skills migration.
  • Expose instruction sources (AGENTS.md) via app server (#17506)
    Addresses #17498
    
    Problem: The TUI derived /status instruction source paths from the local
    client environment, which could show stale <none> output or incorrect
    paths when connected to a remote app server.
    
    Solution: Add an app-server v2 instructionSources snapshot to thread
    start/resume/fork responses, default it to an empty list when older
    servers omit it, and render TUI /status from that server-provided
    session data.
    
    Additional context: The app-server field is intentionally named
    instructionSources rather than AGENTS.md-specific terminology because
    the loaded instruction sources can include global instructions, project
    AGENTS.md files, AGENTS.override.md, user-defined instruction files, and
    future dynamic sources.
  • Handle closed TUI input stream as shutdown (#17430)
    Addresses #17276
    
    Problem: Closing the terminal while the TUI input stream is pending
    could leave the app outside the normal shutdown path, which is risky
    when an approval prompt is active.
    
    Solution: Treat a closed TUI input stream as ShutdownFirst so existing
    thread shutdown behavior cancels pending work and approvals before exit.
  • Pass turn id with feedback uploads (#17314)
    ## Summary
    - Add an optional `tags` dictionary to feedback upload params.
    - Capture the active app-server turn id in the TUI and submit it as
    `tags.turn_id` with `/feedback` uploads.
    - Merge client-provided feedback tags into Sentry feedback tags while
    preserving reserved system fields like `thread_id`, `classification`,
    `cli_version`, `session_source`, and `reason`.
    
    ## Behavior / impact
    Existing feedback upload callers remain compatible because `tags` is
    optional and nullable. The wire shape is still a normal JSON object /
    TypeScript dictionary, so adding future feedback metadata will not
    require a new top-level protocol field each time. This change only adds
    feedback metadata for Codex CLI/TUI uploads; it does not affect existing
    pipelines, DAGs, exports, or downstream consumers unless they choose to
    read the new `turn_id` feedback tag.
    
    ## Tests
    - `cargo fmt -- --config imports_granularity=Item` passed; stable
    rustfmt warned that `imports_granularity` is nightly-only.
    - `cargo run -p codex-app-server-protocol --bin write_schema_fixtures`
    - `cargo test -p codex-feedback
    upload_tags_include_client_tags_and_preserve_reserved_fields`
    - `cargo test -p codex-app-server-protocol
    schema_fixtures_match_generated`
    - `cargo test -p codex-tui build_feedback_upload_params`
    - `cargo test -p codex-tui
    live_app_server_turn_started_sets_feedback_turn_id`
    - `cargo check -p codex-app-server --tests`
    - `git diff --check`
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • TUI: enforce core boundary (#17399)
    Problem: The TUI still depended on `codex-core` directly in a number of
    places, and we had no enforcement from keeping this problem from getting
    worse.
    
    Solution: Route TUI core access through
    `codex-app-server-client::legacy_core`, add CI enforcement for that
    boundary, and re-export this legacy bridge inside the TUI as
    `crate::legacy_core` so the remaining call sites stay readable. There is
    no functional change in this PR — just changes to import targets.
    
    Over time, we can whittle away at the remaining symbols in this legacy
    namespace with the eventual goal of removing them all. In the meantime,
    this linter rule will prevent us from inadvertently importing new
    symbols from core.
  • Revert "Option to Notify Workspace Owner When Usage Limit is Reached" (#17391)
    Reverts openai/codex#16969
    
    #sev3-2026-04-10-accountscheckversion-500s-for-openai-workspace-7300
  • Support clear SessionStart source (#17073)
    ## Motivation
    
    The `SessionStart` hook already receives `startup` and `resume` sources,
    but sessions created from `/clear` previously looked like normal startup
    sessions. This makes it impossible for hook authors to distinguish
    between these with the matcher.
    
    ## Summary
    
    - Add `InitialHistory::Cleared` so `/clear`-created sessions can be
    distinguished from ordinary startup sessions.
    - Add `SessionStartSource::Clear` and wire it through core, app-server
    thread start params, and TUI clear-session flow.
    - Update app-server protocol schemas, generated TypeScript, docs, and
    related tests.
    
    
    https://github.com/user-attachments/assets/9cae3cb4-41c7-4d06-b34f-966252442e5c
  • Option to Notify Workspace Owner When Usage Limit is Reached (#16969)
    ## Summary
    - Replace the manual `/notify-owner` flow with an inline confirmation
    prompt when a usage-based workspace member hits a credits-depleted
    limit.
    - Fetch the current workspace role from the live ChatGPT
    `accounts/check/v4-2023-04-27` endpoint so owner/member behavior matches
    the desktop and web clients.
    - Keep owner, member, and spend-cap messaging distinct so we only offer
    the owner nudge when the workspace is actually out of credits.
    
    ## What Changed
    - `backend-client`
    - Added a typed fetch for the current account role from
    `accounts/check`.
      - Mapped backend role values into a Rust workspace-role enum.
    - `app-server` and protocol
      - Added `workspaceRole` to `account/read` and `account/updated`.
    - Derived `isWorkspaceOwner` from the live role, with a fallback to the
    cached token claim when the role fetch is unavailable.
    - `tui`
      - Removed the explicit `/notify-owner` slash command.
    - When a member is blocked because the workspace is out of credits, the
    error now prompts:
    - `Your workspace is out of credits. Request more from your workspace
    owner? [y/N]`
      - Choosing `y` sends the existing owner-notification request.
    - Choosing `n`, pressing `Esc`, or accepting the default selection
    dismisses the prompt without sending anything.
    - Selection popups now honor explicit item shortcuts, which is how the
    `y` / `n` interaction is wired.
    
    ## Reviewer Notes
    - The main behavior change is scoped to usage-based workspace members
    whose workspace credits are depleted.
    - Spend-cap reached should not show the owner-notification prompt.
    - Owners and admins should continue to see `/usage` guidance instead of
    the member prompt.
    - The live role fetch is best-effort; if it fails, we fall back to the
    existing token-derived ownership signal.
    
    ## Testing
    - Manual verification
      - Workspace owner does not see the member prompt.
    - Workspace member with depleted credits sees the confirmation prompt
    and can send the nudge with `y`.
    - Workspace member with spend cap reached does not see the
    owner-notification prompt.
    
    ### Workspace member out of usage
    
    https://github.com/user-attachments/assets/341ac396-eff4-4a7f-bf0c-60660becbea1
    
    ### Workspace owner
    <img width="1728" height="1086" alt="Screenshot 2026-04-09 at 11 48
    22 AM"
    src="https://github.com/user-attachments/assets/06262a45-e3fc-4cc4-8326-1cbedad46ed6"
    />
  • feat(tui): Ctrl+O copy hotkey and harden copy-as-markdown behavior (#16966)
    ## TL;DR
    
    - New `Ctrl+O` shortcut on top of the existing `/copy` command, allowing
    users to copy the latest agent response without having to cancel a plan
    or type `/copy`
    - Copy server clipboard to the client over SSH (OSC 52)
    - Fixes linux copy behavior: a clipboard handle has to be kept alive
    while the paste happens for the contents to be preserved
    - Uses arboard as primary mechanism on Windows, falling back to
    PowerShell copy clipboard function
    - Works with resumes, rolling back during a session, etc.
    
    Tested on macOS, Linux/X11, Windows WSL2, Windows cmd.exe, Windows
    PowerShell, Windows VSCode PowerShell, Windows VSCode WSL2, SSH (macOS
    -> macOS).
    
    ## Problem
    
    The TUI's `/copy` command was fragile. It relied on a single
    `last_copyable_output` field that was bluntly cleared on every rollback
    and thread reconfiguration, making copied content unavailable after
    common operations like backtracking. It also had no keyboard shortcut,
    requiring users to type `/copy` each time. The previous clipboard
    backend mixed platform selection policy with low-level I/O in a way that
    was hard to test, and it did not keep the Linux clipboard owner alive —
    meaning pasted content could vanish once the process that wrote it
    dropped its `arboard::Clipboard`.
    
    This addresses the text-copy failure modes reported in #12836, #15452,
    and #15663: native Linux clipboard access failing in remote or
    unreachable-display environments, copy state going blank even after
    visible assistant output, and local Linux X11 reporting success while
    leaving the clipboard empty.
    
    ## Shortcut rationale
    
    The copy hotkey is `Ctrl+O` rather than `Alt+C` because Alt/Option
    combinations are not delivered consistently by macOS terminal emulators.
    Terminal.app and iTerm2 can treat Option as text input or as a
    configurable Meta/Esc prefix, and Option+C may be consumed or
    transformed before the TUI sees an `Alt+C` key event. `Ctrl+O` is a
    stable control-key chord in Terminal.app, iTerm2, SSH, and the existing
    cross-platform terminal stack.
    
    ## Mental model
    
    Agent responses are now tracked as a bounded, ordinal-indexed history
    (`agent_turn_markdowns: Vec<AgentTurnMarkdown>`) rather than a single
    nullable string. Each completed agent turn appends an entry keyed by its
    ordinal (the number of user turns seen so far). Rollbacks pop entries
    whose ordinal exceeds the remaining turn count, then use the visible
    transcript cells as a best-effort fallback if the ordinal history no
    longer has a surviving entry. This means `/copy` and `Ctrl+O` reflect
    the most recent surviving agent response after a backtrack, instead of
    going blank.
    
    The clipboard backend was rewritten as `clipboard_copy.rs` with a
    strategy-injection design: `copy_to_clipboard_with` accepts closures for
    the OSC 52, arboard, and WSL PowerShell paths, making the selection
    logic fully unit-testable without touching real clipboards. On Linux,
    the `Clipboard` handle is returned as a `ClipboardLease` stored on
    `ChatWidget`, keeping X11/Wayland clipboard ownership alive for the
    lifetime of the TUI. When native copy fails under WSL, the backend now
    tries the Windows clipboard through PowerShell before falling back to
    OSC 52.
    
    ## Non-goals
    
    - This change does not introduce rich-text (HTML) clipboard support; the
    copied content is raw markdown.
    - It does not add a paste-from-history picker or multi-entry clipboard
    ring.
    - WSL support remains a best-effort fallback, not a new configuration
    surface or guarantee for every terminal/host combination.
    
    ## Tradeoffs
    
    - **Bounded history (256 entries)**: `MAX_AGENT_COPY_HISTORY` caps
    memory. For sessions with thousands of turns this silently drops the
    oldest entries. The cap is generous enough for realistic sessions.
    - **`saw_copy_source_this_turn` flag**: Prevents double-recording when
    both `AgentMessage` and `TurnComplete.last_agent_message` fire for the
    same turn. The flag is reset on turn start and on turn complete,
    creating a narrow window where a race between the two events could
    theoretically skip recording. In practice the protocol delivers them
    sequentially.
    - **Transcript fallback on rollback**:
    `last_agent_markdown_from_transcript` walks the visible transcript cells
    to reconstruct plain text when the ordinal history has been fully
    truncated. This path uses `AgentMessageCell::plain_text()` which joins
    rendered spans, so it reconstructs display text rather than the original
    raw markdown. It keeps visible text copyable after rollback, but
    responses with markdown-specific syntax can diverge from the original
    source.
    - **Clipboard fallback ordering**: SSH still uses OSC 52 exclusively
    because native/PowerShell clipboard access would target the wrong
    machine. Local sessions try native clipboard first, then WSL PowerShell
    when running under WSL, then OSC 52. This adds one process-spawn
    fallback for WSL users but keeps the normal desktop and SSH paths
    simple.
    
    ## Architecture
    
    ```
    chatwidget.rs
    ├── agent_turn_markdowns: Vec<AgentTurnMarkdown>  // ordinal-indexed history
    ├── last_agent_markdown: Option<String>            // always == last entry's markdown
    ├── completed_turn_count: usize                    // incremented when user turns enter history
    ├── saw_copy_source_this_turn: bool                // dedup guard
    ├── clipboard_lease: Option<ClipboardLease>        // keeps Linux clipboard owner alive
    │
    ├── record_agent_markdown(&str)                    // append/update history entry
    ├── truncate_agent_turn_markdowns_to_turn_count()  // rollback support
    ├── copy_last_agent_markdown()                     // public entry point (slash + hotkey)
    └── copy_last_agent_markdown_with(fn)              // testable core
    
    clipboard_copy.rs
    ├── copy_to_clipboard(text) -> Result<Option<ClipboardLease>>
    ├── copy_to_clipboard_with(text, ssh, wsl, osc52_fn, arboard_fn, wsl_fn)
    ├── ClipboardLease { _clipboard on linux }
    ├── arboard_copy(text)          // platform-conditional native clipboard path
    ├── wsl_clipboard_copy(text)    // WSL PowerShell fallback
    ├── osc52_copy(text)            // /dev/tty -> stdout fallback
    ├── SuppressStderr              // macOS stderr redirect guard
    ├── is_ssh_session()
    └── is_wsl_session()
    
    app_backtrack.rs
    ├── last_agent_markdown_from_transcript()  // reconstruct from visible cells
    └── truncate call sites in trim/apply_confirmed_rollback
    ```
    
    ## Observability
    
    - `tracing::warn!` on native clipboard failure before OSC 52 fallback.
    - `tracing::debug!` on `/dev/tty` open/write failure before stdout
    fallback.
    - History cell messages: "Copied last message to clipboard", "Copy
    failed: {error}", "No agent response to copy" appear in the TUI
    transcript.
    
    ## Tests
    
    - `clipboard_copy.rs`: Unit tests cover OSC 52 encoding roundtrip,
    payload size rejection, writer output, SSH-only OSC52 routing, non-WSL
    native-to-OSC52 fallback, WSL native-to-PowerShell fallback, WSL
    PowerShell-to-OSC52 fallback, and all-error reporting via strategy
    injection.
    - `chatwidget/tests/slash_commands.rs`: Updated existing `/copy` tests
    to use `last_agent_markdown_text()` accessor. Added coverage for the
    Linux clipboard lease lifecycle, missing
    `TurnComplete.last_agent_message` fallback through completed assistant
    items, replayed legacy agent messages, stale-output prevention after
    rollback, and the `Ctrl+O` no-output hotkey path.
    - `app_backtrack.rs`: Added
    `agent_group_count_ignores_context_compacted_marker` verifying that
    info-event cells don't inflate the agent group count.
    
    ---------
    
    Co-authored-by: Felipe Coury <felipe.coury@gmail.com>
    Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  • chore: merge name and title (#17116)
    Merge title and name concept to leverage the sqlite title column and
    have more efficient queries
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • feat: /resume per ID/name (#17222)
    Support `/resume 00000-0000-0000-00000000` from the TUI (equivalent for
    the name)
  • Add TUI notification condition config (#17175)
    Problem: TUI desktop notifications are hard-gated on terminal focus, so
    terminal/IDE hosts that want in-focus notifications cannot opt in.
    
    Solution: Add a flat `[tui] notification_condition` setting (`unfocused`
    by default, `always` opt-in), carry grouped TUI notification settings
    through runtime config, apply method + condition together in the TUI,
    and regenerate the config schema.