## TL;DR
This PR changes the `tui_app_server` _path_ in the following ways:
- add missing feature to show agent names (shows only UUIDs today)
- add `Cmd/Alt+Arrows` navigation between agent conversations
## Problem
When the TUI connects to a remote app server, collab agent tool-call
items (spawn, wait, delegate, etc.) render thread UUIDs instead of
human-readable agent names because the `ChatWidget` never receives
nickname/role metadata for receiver threads. Separately, keyboard
next/previous agent navigation silently does nothing when the local
`AgentNavigationState` cache has not yet been populated with subagent
threads that the remote server already knows about.
Both issues share a root cause: in the remote (app-server) code path the
TUI never proactively fetches thread metadata. In the local code path
this metadata arrives naturally via spawn events the TUI itself
orchestrates, but in the remote path those events were processed by a
different client and the TUI only sees the resulting collab tool-call
notifications.
## Mental model
Collab agent tool-call notifications reference receiver threads by id,
but carry no nickname or role. The TUI needs that metadata in two
places:
1. **Rendering** -- `ChatWidget` converts `CollabAgentToolCall` items
into history cells. Without metadata, agent status lines show raw UUIDs.
2. **Navigation** -- `AgentNavigationState` tracks known threads for the
`/agent` picker and keyboard cycling. Without entries for remote
subagents, next/previous has nowhere to go.
This change closes the gap with two complementary strategies:
- **Eager hydration**: when any notification carries
`receiver_thread_ids`, the TUI fetches metadata (`thread/read`) for
threads it has not yet cached before the notification is rendered.
- **Backfill on thread switch**: when the user resumes, forks, or starts
a new app-server thread, the TUI fetches the full `thread/loaded/list`,
walks the parent-child spawn tree, and registers every descendant
subagent in both the navigation cache and the `ChatWidget` metadata map.
A new `collab_agent_metadata` side-table in `ChatWidget` stores
nickname/role keyed by `ThreadId`, kept in sync by `App` whenever it
calls `upsert_agent_picker_thread`. The `replace_chat_widget` helper
re-seeds this map from `AgentNavigationState` so that thread switches
(which reconstruct the widget) do not lose previously discovered
metadata.
## Non-goals
- This change does not alter the local (non-app-server) collab code
path. That path already receives metadata via spawn events and is
unaffected.
- No new protocol messages are introduced. The change uses existing
`thread/read` and `thread/loaded/list` RPCs.
- No changes to how `AgentNavigationState` orders or cycles through
threads. The traversal logic is unchanged; only the population of
entries is extended.
## Tradeoffs
- **Extra RPCs on notification path**:
`hydrate_collab_agent_metadata_for_notification` issues a `thread/read`
for each unknown receiver thread before the notification is forwarded to
rendering. This adds latency on the notification path but only fires
once per thread (the result is cached). The alternative -- rendering
first and backfilling names later -- would cause visible flicker as
UUIDs are replaced with names.
- **Backfill fetches all loaded threads**:
`backfill_loaded_subagent_threads` fetches the full loaded-thread list
and walks the spawn tree even when the user may only care about one
subagent. This is simple and correct but O(loaded_threads) per thread
switch. For typical session sizes this is negligible; it could become a
concern for sessions with hundreds of subagents.
- **Metadata duplication**: agent nickname/role is now stored in both
`AgentNavigationState` (for picker/label) and
`ChatWidget::collab_agent_metadata` (for rendering). The two are kept in
sync through `upsert_agent_picker_thread` and `replace_chat_widget`, but
there is no compile-time enforcement of this coupling.
## Architecture
### New module: `app::loaded_threads`
Pure function `find_loaded_subagent_threads_for_primary` that takes a
flat list of `Thread` objects and a primary thread id, then walks the
`SessionSource::SubAgent` parent-child edges to collect all transitive
descendants. Returns a sorted vec of `LoadedSubagentThread` (thread_id +
nickname + role). No async, no side effects -- designed for unit
testing.
### New methods on `App`
| Method | Purpose |
|--------|---------|
| `collab_receiver_thread_ids` | Extracts `receiver_thread_ids` from
`ItemStarted` / `ItemCompleted` collab notifications |
| `hydrate_collab_agent_metadata_for_notification` | Fetches and caches
metadata for unknown receiver threads before a notification is rendered
|
| `backfill_loaded_subagent_threads` | Bulk-fetches all loaded threads
and registers descendants of the primary thread |
| `adjacent_thread_id_with_backfill` | Attempts navigation, falls back
to backfill if the cache has no adjacent entry |
| `replace_chat_widget` | Replaces the widget and re-seeds its metadata
map from `AgentNavigationState` |
### New state in `ChatWidget`
`collab_agent_metadata: HashMap<ThreadId, CollabAgentMetadata>` -- a
lookup table that rendering functions consult to attach human-readable
names to collab tool-call items. Populated externally by `App` via
`set_collab_agent_metadata`.
### New method on `AppServerSession`
`thread_loaded_list` -- thin wrapper around
`ClientRequest::ThreadLoadedList`.
## Observability
- `tracing::warn` on invalid thread ids during hydration and backfill.
- `tracing::warn` on failed `thread/read` or `thread/loaded/list` RPCs
(with thread id and error).
- No new metrics or feature flags.
## Tests
-
**`loaded_threads::tests::finds_loaded_subagent_tree_for_primary_thread`**
-- unit test for the spawn-tree walk: verifies child and grandchild are
included, unrelated threads are excluded, and metadata is carried
through.
-
**`app::tests::replace_chat_widget_reseeds_collab_agent_metadata_for_replay`**
-- integration test that creates a `ChatWidget`, replaces it via
`replace_chat_widget`, replays a collab wait notification, and asserts
the rendered history cell contains the agent name rather than a UUID.
- **Updated snapshot** `app_server_collab_wait_items_render_history` --
the existing collab wait rendering test now sets metadata before sending
notifications, so the snapshot shows `Robie [explorer]` / `Ada
[reviewer]` instead of raw thread ids.
---------
Co-authored-by: Eric Traut <etraut@openai.com>
## TL;DR
Fix duplicated reasoning summaries in `tui_app_server`.
<img width="1716" height="912" alt="image"
src="https://github.com/user-attachments/assets/6362f25a-ab1c-4a01-bf10-b5616c9428c2"
/>
During live turns, reasoning text is already rendered incrementally from
`ReasoningSummaryTextDelta`. When the same reasoning item later arrives
via `ItemCompleted`, we should only finalize the reasoning block, not
render the same summary again.
## What changed
- only replay rendered reasoning summaries from completed
`ThreadItem::Reasoning` items
- kept live completed reasoning items as finalize-only
- added a regression test covering the live streaming + completion path
## Why
Without this, the first reasoning summary often appears twice in the
transcript when `model_reasoning_summary = "detailed"` and
`features.tui_app_server = true`.
Migrate `cwd` and related session/config state to `AbsolutePathBuf` so
downstream consumers consistently see absolute working directories.
Add test-only `.abs()` helpers for `Path`, `PathBuf`, and `TempDir`, and
update branch-local tests to use them instead of
`AbsolutePathBuf::try_from(...)`.
For the remaining TUI/app-server snapshot coverage that renders absolute
cwd values, keep the snapshots unchanged and skip the Windows-only cases
where the platform-specific absolute path layout differs.
- Remove marketplace from left column.
- Change `Can be installed` to `Available`
- Align right-column marketplace + selected-row hint text across states.
- Changes applied to both `tui` and `tui_app_server`.
- Update related snapshots/tests.
<img width="2142" height="590" alt="image"
src="https://github.com/user-attachments/assets/6e60b783-2bea-46d4-b353-f2fd328ac4d0"
/>
- create `codex-git-utils` and move the shared git helpers into it with
file moves preserved for diff readability
- move the `GitInfo` helpers out of `core` so stacked rollout work can
depend on the shared crate without carrying its own git info module
---------
Co-authored-by: Ahmed Ibrahim <219906144+aibrahim-oai@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
- Remove numeric prefixes for disabled rows in shared list rendering.
These numbers are shortcuts, Ex: Pressing "2" selects option `#2`.
Disabled items can not be selected, so keeping numbers on these items is
misleading.
- Apply the same behavior in both tui and tui_app_server.
- Update affected snapshots for apps/plugins loading and plugin detail
rows.
_**This is a global change.**_
Before:
<img width="1680" height="488" alt="image"
src="https://github.com/user-attachments/assets/4bcf94ad-285f-48d3-a235-a85b58ee58e2"
/>
After:
<img width="1706" height="484" alt="image"
src="https://github.com/user-attachments/assets/76bb6107-a562-42fe-ae94-29440447ca77"
/>
Updates plugin ordering so installed plugins are listed first, with
alphabetical sorting applied within the installed and uninstalled
groups. The behavior is now consistent across both `tui` and
`tui_app_server`, and related tests/snapshots were updated.
Show all plugin marketplaces in the /plugins popup by removing the
`openai-curated` marketplace filter, and update plugin popup
copy/tests/snapshots to match the new behavior in both TUI codepaths.
- add `PreToolUse` hook for bash-like tool execution only at first
- block shell execution before dispatch with deny-only hook behavior
- introduces common.rs matcher framework for matching when hooks are run
example run:
```
› run three parallel echo commands, and the second one should echo "[block-pre-tool-use]" as a test
• Running the three echo commands in parallel now and I’ll report the output directly.
• Running PreToolUse hook: name for demo pre tool use hook
• Running PreToolUse hook: name for demo pre tool use hook
• Running PreToolUse hook: name for demo pre tool use hook
PreToolUse hook (completed)
warning: wizard-tower PreToolUse demo inspected Bash: echo "first parallel echo"
PreToolUse hook (blocked)
warning: wizard-tower PreToolUse demo blocked a Bash command on purpose.
feedback: PreToolUse demo blocked the command. Remove [block-pre-tool-use] to continue.
PreToolUse hook (completed)
warning: wizard-tower PreToolUse demo inspected Bash: echo "third parallel echo"
• Ran echo "first parallel echo"
└ first parallel echo
• Ran echo "third parallel echo"
└ third parallel echo
• Three little waves went out in parallel.
1. printed first parallel echo
2. was blocked before execution because it contained the exact test string [block-pre-tool-use]
3. printed third parallel echo
There was also an unrelated macOS defaults warning around the successful commands, but the echoes
themselves worked fine. If you want, I can rerun the second one with a slightly modified string so
it passes cleanly.
```
## Summary
- route /realtime, Ctrl+C, and deleted realtime meters through the same
realtime stop path
- keep generic transcription placeholder cleanup free of realtime
shutdown side effects
## Testing
- Ran
- Relied on CI for verification; did not run local tests
---------
Co-authored-by: Codex <noreply@openai.com>
## Summary
- queue input after the user submits `/compact` until that manual
compact turn ends
- mirror the same behavior in the app-server TUI
- add regressions for input queued before compact starts and while it is
running
Co-authored-by: Codex <noreply@openai.com>
## Summary
- use Shift+Left to edit the most recent queued message when running
under tmux
- mirror the same binding change in the app-server TUI
- add tmux-specific tests and snapshot coverage for the rendered
queued-message hint
## Testing
- just fmt
- cargo test -p codex-tui
- cargo test -p codex-tui-app-server
- just argument-comment-lint -p codex-tui -p codex-tui-app-server
Co-authored-by: Codex <noreply@openai.com>
### Preliminary /plugins TUI menu
- Adds a preliminary /plugins menu flow in both tui and tui_app_server.
- Fetches plugin list data asynchronously and shows loading/error/cached
states.
- Limits this first pass to the curated ChatGPT marketplace.
- Shows available plugins with installed/status metadata.
- Supports in-menu search over plugin display name, plugin id, plugin
name, and marketplace label.
- Opens a plugin detail view on selection, including summaries for
Skills, Apps, and MCP Servers, with back navigation.
### Testing
- Launch codex-cli with plugins enabled (`--enable plugins`).
- Run /plugins and verify:
- loading state appears first
- plugin list is shown
- search filters results
- selecting a plugin opens detail view, with a list of
skills/connectors/MCP servers for the plugin
- back action returns to the list.
- Verify disabled behavior by running /plugins without plugins enabled
(shows “Plugins are disabled” message).
- Launch with `--enable tui_app_server` (and plugins enabled) and repeat
the same /plugins flow; behavior should match.
## Why
The argument-comment lint now has a packaged DotSlash artifact from
[#15198](https://github.com/openai/codex/pull/15198), so the normal repo
lint path should use that released payload instead of rebuilding the
lint from source every time.
That keeps `just clippy` and CI aligned with the shipped artifact while
preserving a separate source-build path for people actively hacking on
the lint crate.
The current alpha package also exposed two integration wrinkles that the
repo-side prebuilt wrapper needs to smooth over:
- the bundled Dylint library filename includes the host triple, for
example `@nightly-2025-09-18-aarch64-apple-darwin`, and Dylint derives
`RUSTUP_TOOLCHAIN` from that filename
- on Windows, Dylint's driver path also expects `RUSTUP_HOME` to be
present in the environment
Without those adjustments, the prebuilt CI jobs fail during `cargo
metadata` or driver setup. This change makes the checked-in prebuilt
wrapper normalize the packaged library name to the plain
`nightly-2025-09-18` channel before invoking `cargo-dylint`, and it
teaches both the wrapper and the packaged runner source to infer
`RUSTUP_HOME` from `rustup show home` when the environment does not
already provide it.
After the prebuilt Windows lint job started running successfully, it
also surfaced a handful of existing anonymous literal callsites in
`windows-sandbox-rs`. This PR now annotates those callsites so the new
cross-platform lint job is green on the current tree.
## What Changed
- checked in the current
`tools/argument-comment-lint/argument-comment-lint` DotSlash manifest
- kept `tools/argument-comment-lint/run.sh` as the source-build wrapper
for lint development
- added `tools/argument-comment-lint/run-prebuilt-linter.sh` as the
normal enforcement path, using the checked-in DotSlash package and
bundled `cargo-dylint`
- updated `just clippy` and `just argument-comment-lint` to use the
prebuilt wrapper
- split `.github/workflows/rust-ci.yml` so source-package checks live in
a dedicated `argument_comment_lint_package` job, while the released lint
runs in an `argument_comment_lint_prebuilt` matrix on Linux, macOS, and
Windows
- kept the pinned `nightly-2025-09-18` toolchain install in the prebuilt
CI matrix, since the prebuilt package still relies on rustup-provided
toolchain components
- updated `tools/argument-comment-lint/run-prebuilt-linter.sh` to
normalize host-qualified nightly library filenames, keep the `rustup`
shim directory ahead of direct toolchain `cargo` binaries, and export
`RUSTUP_HOME` when needed for Windows Dylint driver setup
- updated `tools/argument-comment-lint/src/bin/argument-comment-lint.rs`
so future published DotSlash artifacts apply the same nightly-filename
normalization and `RUSTUP_HOME` inference internally
- fixed the remaining Windows lint violations in
`codex-rs/windows-sandbox-rs` by adding the required `/*param*/`
comments at the reported callsites
- documented the checked-in DotSlash file, wrapper split, archive
layout, nightly prerequisite, and Windows `RUSTUP_HOME` requirement in
`tools/argument-comment-lint/README.md`
- Split the feature system into a new `codex-features` crate.
- Cut `codex-core` and workspace consumers over to the new config and
warning APIs.
Co-authored-by: Ahmed Ibrahim <219906144+aibrahim-oai@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
- Move core/src/terminal.rs and its tests into a standalone
terminal-detection workspace crate.
- Update direct consumers to depend on codex-terminal-detection and
import terminal APIs directly.
---------
Co-authored-by: Codex <noreply@openai.com>
This PR adds a new `thread/shellCommand` app server API so clients can
implement `!` shell commands. These commands are executed within the
sandbox, and the command text and output are visible to the model.
The internal implementation mirrors the current TUI `!` behavior.
- persist shell command execution as `CommandExecution` thread items,
including source and formatted output metadata
- bridge live and replayed app-server command execution events back into
the existing `tui_app_server` exec rendering path
This PR also wires `tui_app_server` to submit `!` commands through the
new API.
- close live realtime sessions on errors, ctrl-c, and active meter
removal
- centralize TUI realtime cleanup and avoid duplicate follow-up close
info
---------
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Ahmed Ibrahim <219906144+aibrahim-oai@users.noreply.github.com>
## Problem
The `/mcp` command did not work in the app-server TUI (remote mode). On
`main`, `add_mcp_output()` called `McpManager::effective_servers()`
in-process, which only sees locally configured servers, and then emitted
a generic stub message for the app-server to handle. In remote usage,
that left `/mcp` without a real inventory view.
## Solution
Implement `/mcp` for the app-server TUI by fetching MCP server inventory
directly from the app-server via the paginated `mcpServerStatus/list`
RPC and rendering the results into chat history.
The command now follows a three-phase lifecycle:
1. Loading: `ChatWidget::add_mcp_output()` inserts a transient
`McpInventoryLoadingCell` and emits `AppEvent::FetchMcpInventory`. This
gives immediate feedback that the command registered.
2. Fetch: `App::fetch_mcp_inventory()` spawns a background task that
calls `fetch_all_mcp_server_statuses()` over an app-server request
handle. When the RPC completes, it sends `AppEvent::McpInventoryLoaded {
result }`.
3. Resolve: `App::handle_mcp_inventory_result()` clears the loading cell
and renders either `new_mcp_tools_output_from_statuses(...)` or an error
message.
This keeps the main app event loop responsive, so the TUI can repaint
before the remote RPC finishes.
## Notes
- No `app-server` changes were required.
- The rendered inventory includes auth, tools, resources, and resource
templates, plus transport details when they are available from local
config for display enrichment.
- The app-server RPC does not expose authoritative `enabled` or
`disabled_reason` state for MCP servers, so the remote `/mcp` view no
longer renders a `Status:` row rather than guessing from local config.
- RPC failures surface in history as `Failed to load MCP inventory:
...`.
## Tests
- `slash_mcp_requests_inventory_via_app_server`
- `mcp_inventory_maps_prefix_tool_names_by_server`
- `handle_mcp_inventory_result_clears_committed_loading_cell`
- `mcp_tools_output_from_statuses_renders_status_only_servers`
- `mcp_inventory_loading_snapshot`
## Problem
When the TUI connects to a **remote** app-server (via WebSocket), resume
and fork operations lost all conversation history.
`AppServerStartedThread` carried only the `SessionConfigured` event, not
the full `Thread` snapshot. After resume or fork, the chat transcript
was empty — prior turns were silently discarded.
A secondary issue: `primary_session_configured` was not cleared on
reset, causing stale session state after reconnection.
## Approach: TUI-side only, zero app-server changes
The app-server **already returns** the full `Thread` object (with
populated `turns: Vec<Turn>`) in its `ThreadStartResponse`,
`ThreadResumeResponse`, and `ThreadForkResponse`. The data was always
there — the TUI was simply throwing it away. The old
`AppServerStartedThread` struct only kept the `SessionConfiguredEvent`,
discarding the rich turn history that the server had already provided.
This PR fixes the problem entirely within `tui_app_server` (3 files
changed, 0 changes to `app-server`, `app-server-protocol`, or any other
crate). Rather than modifying the server to send history in a different
format or adding a new endpoint, the fix preserves the existing `Thread`
snapshot and replays it through the TUI's standard event pipeline —
making restored sessions indistinguishable from live ones.
## Solution
Add a **thread snapshot replay** path. When the server hands back a
`Thread` object (on start, resume, or fork),
`restore_started_app_server_thread` converts its historical turns into
the same core `Event` sequence the TUI already processes for live
interactions, then replays them into the event store so the chat widget
renders them.
Key changes:
- **`AppServerStartedThread` now carries the full `Thread`** —
`started_thread_from_{start,resume,fork}_response` clone the thread into
the struct alongside the existing `SessionConfiguredEvent`.
- **`thread_snapshot_events()`** walks the thread's turns and items,
producing `TurnStarted` → `ItemCompleted`* →
`TurnComplete`/`TurnAborted` event sequences that the TUI already knows
how to render.
- **`restore_started_app_server_thread()`** pushes the session event +
history events into the thread channel's store, activates the channel,
and replays the snapshot — used for initial startup, resume, and fork.
- **`primary_session_configured` cleared on reset** to prevent stale
session state after reconnection.
## Tradeoffs
- **`Thread` is cloned into `AppServerStartedThread`**: The full thread
snapshot (including all historical turns) is cloned at startup. For
long-lived threads this could be large, but it's a one-time cost and
avoids lifetime gymnastics with the response.
## Tests
- `restore_started_app_server_thread_replays_remote_history` —
end-to-end: constructs a `Thread` with one completed turn, restores it,
and asserts user/agent messages appear in the transcript.
- `bridges_thread_snapshot_turns_for_resume_restore` — unit: verifies
`thread_snapshot_events` produces the correct event sequence for
completed and interrupted turns.
## Test plan
- [ ] Verify `cargo check -p codex-tui-app-server` passes
- [ ] Verify `cargo test -p codex-tui-app-server` passes
- [ ] Manual: connect to a remote app-server, resume an existing thread,
confirm history renders in the chat widget
- [ ] Manual: fork a thread via remote, confirm prior turns appear
## Stack Position
2/4. Built on top of #14828.
## Base
- #14828
## Unblocks
- #14829
- #14827
## Scope
- Port the realtime v2 wire parsing, session, app-server, and
conversation runtime behavior onto the split websocket-method base.
- Branch runtime behavior directly on the current realtime session kind
instead of parser-derived flow flags.
- Keep regression coverage in the existing e2e suites.
---------
Co-authored-by: Codex <noreply@openai.com>
## Why
Once the repo-local lint exists, `codex-rs` needs to follow the
checked-in convention and CI needs to keep it from drifting. This commit
applies the fallback `/*param*/` style consistently across existing
positional literal call sites without changing those APIs.
The longer-term preference is still to avoid APIs that require comments
by choosing clearer parameter types and call shapes. This PR is
intentionally the mechanical follow-through for the places where the
existing signatures stay in place.
After rebasing onto newer `main`, the rollout also had to cover newly
introduced `tui_app_server` call sites. That made it clear the first cut
of the CI job was too expensive for the common path: it was spending
almost as much time installing `cargo-dylint` and re-testing the lint
crate as a representative test job spends running product tests. The CI
update keeps the full workspace enforcement but trims that extra
overhead from ordinary `codex-rs` PRs.
## What changed
- keep a dedicated `argument_comment_lint` job in `rust-ci`
- mechanically annotate remaining opaque positional literals across
`codex-rs` with exact `/*param*/` comments, including the rebased
`tui_app_server` call sites that now fall under the lint
- keep the checked-in style aligned with the lint policy by using
`/*param*/` and leaving string and char literals uncommented
- cache `cargo-dylint`, `dylint-link`, and the relevant Cargo
registry/git metadata in the lint job
- split changed-path detection so the lint crate's own `cargo test` step
runs only when `tools/argument-comment-lint/*` or `rust-ci.yml` changes
- continue to run the repo wrapper over the `codex-rs` workspace, so
product-code enforcement is unchanged
Most of the code changes in this commit are intentionally mechanical
comment rewrites or insertions driven by the lint itself.
## Verification
- `./tools/argument-comment-lint/run.sh --workspace`
- `cargo test -p codex-tui-app-server -p codex-tui`
- parsed `.github/workflows/rust-ci.yml` locally with PyYAML
---
* -> #14652
* #14651
This PR replicates the `tui` code directory and creates a temporary
parallel `tui_app_server` directory. It also implements a new feature
flag `tui_app_server` to select between the two tui implementations.
Once the new app-server-based TUI is stabilized, we'll delete the old
`tui` directory and feature flag.