Path: @/codex-rs/tui
The codex-tui crate provides the interactive terminal user interface for Codex, built with the Ratatui framework. It handles the fullscreen TUI experience including chat display, input composition, onboarding flows, session management, and real-time streaming of model responses with markdown rendering.
TUI is one of the primary entry points, invoked when running codex without a subcommand:
- Depends on
codex-corefor conversation management, configuration, and authentication - Depends on
codex-acpfor ACP agent backend (alternative to HTTP-based LLM providers) - Depends on
codex-commonfor CLI argument parsing and shared utilities - Uses
codex-protocoltypes for events and messages - Integrates
codex-feedbackfor tracing/feedback collection
The cli/ crate's main.rs dispatches to codex_tui::run_main() for interactive mode.
Entry Point:
run_main() in lib.rs:
- Parses CLI arguments and loads configuration
- Initializes tracing (file + OpenTelemetry)
- Runs onboarding if needed (login, trust screen)
- Handles session resume selection
- Launches the main
App::run()loop
Application Core:
app.rs: MainAppstruct managing application state and event loopapp_event.rs: Application-level events (key input, model responses, etc.)tui.rs: Terminal initialization and restoration
Agent Spawning (chatwidget/agent.rs):
The TUI supports two backend modes, selected automatically at startup based on model name:
spawn_agent(): Entry point that detects ACP vs HTTP mode viacodex_acp::get_agent_config()spawn_acp_agent(): UsesAcpBackendfor ACP-registered models (e.g., "mock-model", "mock-model-alt", "claude-acp", "gemini-acp")spawn_http_agent(): Usescodex-corefor HTTP-based LLM providers (OpenAI, Anthropic, etc.)spawn_error_agent(): Displays error and exits for unregistered models when HTTP fallback is disabled
Both backends produce codex_protocol::Event for the TUI event loop, enabling unified event handling.
ACP Backend Arc Reference Handling:
In spawn_acp_agent(), the main task must drop its Arc<AcpBackend> reference after spawning the op forwarding task. This prevents a self-reference deadlock:
- The op task holds
Arc<AcpBackend>for submitting operations - The backend contains
event_txinternally - The main task waits on
event_rxfor events - If the main task also held an Arc reference, dropping the backend would require the main task to exit first, but the main task waits on
event_rx, which can't close untilevent_txis dropped - Solution:
drop(backend)after spawning the op task, so when the op channel closes (whencodex_op_rxcloses), the backend is fully dropped, closingevent_txand allowingevent_rxto returnNone
UI Components:
chatwidget.rs: Main conversation display widgetbottom_pane.rs: Status bar and key hintsmarkdown_render.rs/markdown_stream.rs: Markdown to Ratatui renderingdiff_render.rs: Patch diff visualizationselection_list.rs: Generic selection popup widgetshimmer.rs: Loading animation effectsstatus_indicator_widget.rs: Status displaynori/: Nori-specific branding and customization (see@/codex-rs/tui/src/nori/docs.md)
Input Handling:
public_widgets/composer_input.rs: Text input with multi-line supportclipboard_paste.rs: Clipboard integrationslash_command.rs:/commandparsing and executionfile_search.rs: Fuzzy file finder
ACP Agent Switching:
/agentnow opens the Nori-specific agent picker popup intui/src/chatwidget.rs, which drivesnori::agent_picker::agent_picker_params()and renders the metadata returned bycodex_acp::list_available_agents()asSelectionItems.- Selecting an agent sends
AppEvent::SetPendingAgent, so both the App andChatWidgetstore apending_agent(seePendingAgentSelectionandPendingAgentInfo). The UI informs the user that the switch will happen on the next prompt submission. - When the next prompt is submitted,
ChatWidgetintercepts the queuedUserMessage, forwards it asAppEvent::SubmitWithAgentSwitch, and lets the App restart the conversation with the new model (clearing the pending flag, updatingConfig, shutting down the old conversation, and creating aChatWidgetwithexpected_modelto filter out leftover events). /modelnow checkscodex_acp::get_agent_config(); if the workspace is in ACP mode it shows the disabledacp_model_picker_params()view that explicitly tells users to use/agentinstead of selecting models directly.- This workflow avoids disrupting active turns and powers the agent-switching verification in
tui-pty-e2e/tests/agent_switching.rs, including the message-flow and pending-selection tests added in the last commits.
Onboarding:
The onboarding/ module handles first-run experience:
- Login screen (ChatGPT OAuth or API key)
- Trust screen (directory permission settings)
- Windows WSL setup instructions
Session Management:
resume_picker.rs: UI for selecting sessions to resumesession_log.rs: High-fidelity session event logging
Rendering Patterns:
The crate uses Ratatui's Stylize trait for concise styling:
// Preferred
"text".red(), "text".dim(), vec![...].into()
// Avoid
Span::styled("text", Style::default().fg(Color::Red))Text wrapping uses textwrap::wrap for plain strings and custom wrapping.rs helpers for styled Line objects.
Markdown Streaming:
markdown_stream.rs handles incremental markdown rendering as tokens arrive, maintaining rendering state across deltas for smooth display updates.
Event Loop Architecture:
The app uses a tokio-based event loop that multiplexes:
- Terminal input events (crossterm)
- Model response events (from core)
- Timers for animations
State updates flow through app_event_sender.rs channels.
Interrupt Queueing and Approval Handling:
Most event types (exec begin/end, MCP calls, elicitation) are queued during active streaming and flushed when streaming completes via InterruptManager. However, approval requests are handled immediately (not deferred):
on_exec_approval_request()andon_apply_patch_approval_request()call their handlers directly- This prevents deadlocks in ACP mode where the agent subprocess blocks waiting for approval
- If approval were deferred, the agent would wait for approval, but TaskComplete (which flushes the queue) wouldn't arrive until the agent finished
- The
InterruptManagerstill containsExecApprovalandApplyPatchApprovalvariants for completeness, but these methods are marked#[allow(dead_code)] on_task_complete()callsflush_interrupt_queue()for any remaining queued items
Pending ExecCell Tracking:
The PendingExecCellTracker (chatwidget/pending_exec_cells.rs) prevents duplicate ACP tool call messages in the chat history. The problem it solves:
- Agent makes a tool call (e.g.,
shell) which creates an ExecCell inactive_cell - Agent streams text during the tool call execution
- Streaming text causes
flush_active_cell(), which would normally push the incomplete ExecCell to history and clearactive_cell - When
ExecCommandEndarrives,handle_exec_end_now()would create a new ExecCell sinceactive_cellis empty - Result: duplicate entries for the same tool call
The tracker intercepts this by:
save_pending(): Called during flush if the ExecCell has pending (incomplete) call_ids - saves the cell keyed by call_id instead of pushing to historyretrieve(): Called inhandle_exec_end_now()- retrieves and removes the saved cell, restoring it toactive_cellfor completiondrain_failed(): Called inon_task_complete()- marks any uncompleted pending cells as failed and returns them for insertion into history
This follows the same encapsulation pattern as InterruptManager: self-contained state in its own module file with typed public methods instead of exposing raw data structures.
ACP File Tracing:
- The TUI calls
codex_acp::init_file_tracing()at startup (tui/src/lib.rs) to write.codex-acp.login the current directory. Every mock agent logsACP agent spawned (pid: ...)there, which makes the agent-switching tests intui-pty-e2edeterministic and ensures developers can inspect agent subprocess lifecycles during debugging.
Agent Switch Event Filtering:
When switching between ACP agents (e.g., via /agent command), ChatWidget uses an event filtering mechanism to prevent race conditions:
expected_model: Option<String>inChatWidgetInitspecifies which model the widget expectssession_configured_received: booltracks whetherSessionConfiguredhas arrived from the expected model- When
expected_modelis set,handle_codex_event()filters events:- All events are ignored until
SessionConfiguredarrives SessionConfiguredis only accepted ifevent.modelmatchesexpected_model(case-insensitive)- Once matching
SessionConfiguredarrives,session_configured_receivedis set totrueand normal event processing resumes
- All events are ignored until
- This prevents the OLD agent's final events (completion, shutdown) from being processed by the NEW agent's widget
- Fresh sessions, resumed sessions, and
/newcommand useexpected_model: None(no filtering)
Color System:
The color.rs and terminal_palette.rs modules handle terminal color detection and theming. The app queries terminal colors at startup for theme adaptation.
Test Infrastructure:
test_backend.rs: Test terminal backend for snapshot testing- Uses
instafor snapshot tests of rendered output AGENTS.mddocuments testing conventions- Black-box integration tests in
@/codex-rs/tui-pty-e2etest full TUI via PTY - Integration tests spawn real
codexbinary withmock-acp-agentbackend
Configuration Flow:
TUI respects config overrides from:
- CLI flags (
--model,--sandbox, etc.) -c key=valueoverrides- Config profiles (
-p profile-name) ~/.codex/config.toml
Created and maintained by Nori.