The Terminal User Interface (TUI) provides an interactive, terminal-based interface for Codex sessions. It implements a full-featured chat UI with streaming output, history management, and interactive controls using the ratatui rendering framework. This document covers the overall TUI architecture, component organization, and integration with the core agent system. For details on specific subsystems, see App Event Loop and Initialization, ChatWidget and Conversation Display, BottomPane and Input System, Status Line and Footer Rendering, and Interactive Overlays and Approvals.
The TUI is organized as a layered event-driven system where user input and protocol events flow through well-defined component boundaries. The rendering layer uses ratatui to draw to the terminal, while business logic is handled by stateful widgets that process events and update their internal state.
TUI Component Hierarchy
Sources: codex-rs/tui/src/app.rs518-584 codex-rs/tui/src/chatwidget.rs515-665 codex-rs/tui/src/bottom_pane/mod.rs145-174
The TUI maintains a clear separation between presentation (rendering) and state management (event processing). The Tui struct (codex-rs/tui/src/tui.rs) manages the terminal backend and frame scheduling, while App owns the session state and routes events to ChatWidget and BottomPane.
The App struct is the top-level coordinator that owns the agent connection, UI state, and event multiplexing logic. It manages the event loop, coordinates between UI widgets and the core agent, and handles global operations like session switching and shutdown.
Key responsibilities:
TuiEvent (keyboard/mouse/timer) and AppEvent (UI-internal commands)ThreadManager to ChatWidgetThreadManager for multi-session supportCore fields:
| Field | Type | Purpose |
|---|---|---|
server | Arc<ThreadManager> | Multi-session agent manager |
chat_widget | ChatWidget | Main conversation display |
app_event_tx | AppEventSender | UI event channel sender |
transcript_cells | Vec<Arc<dyn HistoryCell>> | Committed history |
overlay | Option<Overlay> | Transcript/diff pager overlay |
thread_event_channels | HashMap<ThreadId, ThreadEventChannel> | Per-thread event routing |
Sources: codex-rs/tui/src/app.rs518-584
ChatWidget maintains the conversation UI state and processes protocol events from the agent. It builds history cells from streaming events, manages the active (in-flight) cell, and coordinates with BottomPane for input handling.
Key responsibilities:
Event messages from codex-coreHistoryCell instances for conversation displayActive cell pattern:
The active_cell field holds a mutable history cell that updates in-place during streaming. Once a turn completes, the active cell is moved to committed history and a new cell can begin. The active_cell_revision counter tracks in-place mutations so the transcript overlay knows when to invalidate its cached "live tail" rendering.
Sources: codex-rs/tui/src/chatwidget.rs515-665 codex-rs/tui/src/chatwidget.rs1-28
The BottomPane owns the input surface, routing keys to either a transient view (popup/modal) or the ChatComposer. ChatComposer manages the editable text buffer, history navigation, slash command completion, and file/skill mention popups.
Key responsibilities:
The composer supports rich input features including:
@filename)$skillname)Sources: codex-rs/tui/src/bottom_pane/mod.rs145-174 codex-rs/tui/src/bottom_pane/chat_composer.rs287-339
The TUI uses two primary event channels to decouple UI actions from protocol operations:
Key Event Processing Flow
Sources: codex-rs/tui/src/app.rs635-913 codex-rs/tui/src/chatwidget.rs2370-2900
AppEvent is an enum of UI-level commands that widgets emit when they need app-layer coordination. Examples include opening pickers, persisting configuration, or requesting shutdown. The App::run loop processes these events and dispatches to appropriate handlers.
Common AppEvent patterns:
| Event | Handler | Purpose |
|---|---|---|
CodexEvent(Event) | ChatWidget::handle_codex_event | Forward protocol event |
CodexOp(Op) | Codex::submit | Send operation to agent |
InsertHistoryCell | App::transcript_cells.push | Commit display cell |
OpenResumePicker | App::run_resume_picker | Launch session picker |
Exit(ExitMode) | App::run loop break | Initiate shutdown |
PersistModelSelection | App::persist_model_selection | Write config update |
Sources: codex-rs/tui/src/app_event.rs45-371 codex-rs/tui/src/app.rs635-913
The TUI rendering loop is driven by the Tui struct, which schedules frames at a target interval (approximately 60 FPS). The App implements the Renderable trait to draw the full UI layout.
Rendering Architecture
Sources: codex-rs/tui/src/tui.rs codex-rs/tui/src/app.rs1463-1605
The Renderable trait defines render(&self, area: Rect, buf: &mut Buffer) for drawing widgets. Most widgets also implement desired_height(width: u16) to support layout calculations before rendering.
Layout structure:
The layout uses ratatui's Layout to split the terminal into these regions, with dynamic height allocation based on content and constraints.
The TUI connects to codex-core through the ThreadManager and Codex interfaces. ThreadManager provides multi-session support, while individual Codex instances represent agent sessions.
Thread State Machine
The TUI supports multiple concurrent threads via ThreadManager, but only one thread is "active" at a time (receives keyboard input and displays events). Users can switch threads using the agent picker or by resuming/forking sessions.
Thread event buffering:
Each thread has a ThreadEventChannel that buffers recent events. When switching to a previously active thread, the TUI can replay buffered events to restore UI state without re-reading the rollout file.
Sources: codex-rs/tui/src/app.rs246-349 codex-rs/core/src/codex.rs272-520
When the user submits input, the TUI constructs a Submission with a unique ID and an Op payload. The most common submission is Op::UserTurn, which includes full turn context (cwd, approval policy, sandbox policy, model, etc.).
UserTurn construction:
User presses Enter
→ BottomPane::handle_key_event returns InputResult::Submitted
→ App extracts text + text_elements + local_images + remote_image_urls
→ App builds Vec<UserInput> (images first, then text)
→ App constructs Op::UserTurn with current policies
→ App calls Codex::submit(op)
→ Codex sends Submission to session loop
The UserInput enum supports multiple input types:
Text { text, text_elements } - Markdown text with styled rangesLocalImage { path } - Attached image fileImage { image_url } - Remote image URLSources: codex-rs/tui/src/app.rs1607-1738 codex-rs/core/src/codex.rs469-485
Protocol events arrive via the Codex::rx_event channel and are forwarded to ChatWidget::handle_codex_event. The widget updates its internal state (active cell, history, token usage) and emits AppEvent messages when UI-layer actions are needed.
Common event handling patterns:
| Event | ChatWidget Action | Side Effects |
|---|---|---|
TurnStarted | Create new turn state | Start commit animation |
AgentMessageDelta | Append to active cell buffer | None |
ExecCommandBegin | Create or update ExecCell | None |
ExecCommandOutputDelta | Append output to ExecCell | None |
ExecApprovalRequest | Emit FullScreenApprovalRequest | App pushes ApprovalOverlay |
ItemCompleted | Commit active cell | Insert into transcript |
TurnComplete | Finalize turn state | Stop commit animation |
Sources: codex-rs/tui/src/chatwidget.rs2370-2900
The HistoryCell trait defines the interface for conversation history items. Each cell produces logical lines for rendering and reports how many viewport rows it occupies. Cells can be committed (immutable) or active (mutated in-place during streaming).
Core trait methods:
trait HistoryCell {
fn display_lines(&self, width: u16) -> Vec<Line<'static>>;
fn desired_height(&self, width: u16) -> u16;
fn transcript_lines(&self, width: u16) -> Vec<Line<'static>>;
fn is_stream_continuation(&self) -> bool;
fn transcript_animation_tick(&self) -> Option<u64>;
}
Common cell types:
| Type | Purpose | Mutability |
|---|---|---|
UserHistoryCell | User message with images | Immutable |
AgentMessageCell | Assistant text response | Immutable |
ExecCell | Tool call group (shell/patch/MCP) | Mutable while streaming |
WebSearchCell | Web search operation | Mutable while in-flight |
PlainHistoryCell | Static content (warnings, notices) | Immutable |
ReasoningSummaryCell | Reasoning header summary | Immutable |
The active_cell field in ChatWidget holds a Box<dyn HistoryCell> that can be downcast to its concrete type for in-place updates. Once a turn completes, the active cell is committed to App::transcript_cells as an Arc<dyn HistoryCell>.
Sources: codex-rs/tui/src/history_cell.rs82-194 codex-rs/tui/src/chatwidget.rs519-529
pub(crate) struct App {
server: Arc<ThreadManager>,
otel_manager: OtelManager,
app_event_tx: AppEventSender,
chat_widget: ChatWidget,
auth_manager: Arc<AuthManager>,
config: Config,
transcript_cells: Vec<Arc<dyn HistoryCell>>,
overlay: Option<Overlay>,
thread_event_channels: HashMap<ThreadId, ThreadEventChannel>,
active_thread_id: Option<ThreadId>,
primary_thread_id: Option<ThreadId>,
// ... additional fields
}
Sources: codex-rs/tui/src/app.rs518-584
pub(crate) struct ChatWidget {
app_event_tx: AppEventSender,
codex_op_tx: UnboundedSender<Op>,
bottom_pane: BottomPane,
active_cell: Option<Box<dyn HistoryCell>>,
active_cell_revision: u64,
config: Config,
// Stream lifecycle controllers
stream_controller: Option<StreamController>,
plan_stream_controller: Option<PlanStreamController>,
// Model/token tracking
token_info: Option<TokenUsageInfo>,
rate_limit_snapshots_by_limit_id: BTreeMap<String, RateLimitSnapshotDisplay>,
// Skills and connectors
skills_all: Vec<ProtocolSkillMetadata>,
connectors_cache: ConnectorsCacheState,
// ... additional fields
}
Sources: codex-rs/tui/src/chatwidget.rs515-665
pub(crate) struct BottomPane {
composer: ChatComposer,
view_stack: Vec<Box<dyn BottomPaneView>>,
app_event_tx: AppEventSender,
frame_requester: FrameRequester,
status: Option<StatusIndicatorWidget>,
unified_exec_footer: UnifiedExecFooter,
queued_user_messages: QueuedUserMessages,
is_task_running: bool,
has_input_focus: bool,
// ... additional fields
}
Refresh this wiki