Start from an ordinary turn. The assistant is streaming text. A command starts, produces output, and completes. A permission prompt may need a user response. The turn finally moves from running to completed. Later, the same thread can be resumed and the transcript appears again.

The source does not treat those as separate UI tricks. Core emits and persists facts; app-server converts them into JSON-RPC notifications; TUI turns notifications into history cells and status lines; rollout keeps the records needed for resume. This article follows those concrete conversions from source event to visible state.

A conversion takes one runtime event and produces the data structure needed by another module. The result may be an EventMsg, a ServerNotification, a ThreadItem, a TUI history cell, or a rollout record. First find the converting module and its input and output types; then inspect how the UI uses the result.

Source scope. This article only describes event types, mapping functions, notification structs, TUI handlers, and rollout reconstruction visible in the public openai/codex source. It does not infer private product UI internals.

1. Separate the Shapes First

The terms are easy to blur because several of them sound like history or events. Different modules use them for different jobs.

Shape Owner Question answered
EventMsg core protocol What runtime fact just happened?
TurnItem core item stream Which response item is worth exposing as a turn item?
ThreadItem app-server v2 protocol How should clients receive transcript items?
ServerNotification app-server JSON-RPC Which notification should clients observe?
history cell TUI How should this fact render in terminal scrollback or live status?
rollout record persistence / resume Which fact must survive so the thread can be reconstructed?

A message delta can update a live TUI stream immediately. The completed assistant message later becomes a stable item. Those two representations serve different lifetimes: one is live feedback, the other is replayable history.

2. Core Decides What Becomes a Turn Item

Client-visible items are narrowed before app-server rendering. parse_turn_item maps provider ResponseItem values into TurnItem values for user messages, assistant messages, reasoning, web search, and image generation. It also filters contextual fragments such as permission, skills, and collaboration-mode payloads that are runtime context rather than visible transcript.

Completed response items pass through record_response_item_and_emit_turn_item: record the response item, parse a turn item, then emit item-started and item-completed events when a visible item exists. record_conversation_items updates in-memory history, persists rollout response items, and emits RawResponseItem. Raw response, turn item, and UI item are therefore separate layers.

pub(crate) async fn record_response_item_and_emit_turn_item(
    &self,
    turn_context: &TurnContext,
    model_info: &ModelInfo,
    response_item: ResponseItem,
) {
    self.record_conversation_items(turn_context, model_info, std::slice::from_ref(&response_item))
        .await;

    if let Some(item) = parse_turn_item(&response_item) {
        self.emit_turn_item_started(turn_context, &item).await;
        self.emit_turn_item_completed(turn_context, item).await;
    }
}

The useful lesson is the order: Codex records the provider response as conversation state first, then asks whether it can become a client-visible turn item. App-server and TUI therefore receive a narrowed runtime fact, not an arbitrary slice of the raw provider payload.

3. App-Server Converts Facts Into Notifications

The app-server entry point is apply_bespoke_event_handling. It receives core events and maps them to app-server protocol messages. TurnStarted clears pending server requests, notes the running turn, and sends ServerNotification::TurnStarted. Turn completion aborts pending requests and emits a completion notification with status.

The wire names live in ServerNotification: turn/started, turn/completed, item/started, item/completed, and delta notifications. The v2 Turn shape carries items, items_view, status, errors, and timing metadata.

3.1 App-Server Derives Turn Status From Events

Turn start sends an in-progress turn with unloaded items. Completion goes through handle_turn_complete, which reads app-server turn summary state and chooses Completed or Failed. Then emit_turn_completed_with_status sends the client-facing shape.

3.2 Item Lifecycle and Deltas Use Different Channels

Stateless one-to-one mappings use item_event_to_server_notification. It maps item lifecycle events to item notifications, agent text deltas to AgentMessageDelta, and command output bytes to CommandExecutionOutputDelta. Some paths add app-server state: command begin/end events are deduplicated, and legacy command items are suppressed for unified exec interactions so clients do not render duplicate wait states.

3.3 Approval Uses a Request That Expects a Reply

Approval events become server requests because the client must answer. ApplyPatchApprovalRequest and ExecApprovalRequest send approval requests and await responses. They are not passive history notifications.

4. TUI Converts Notifications Into Terminal State

TUI first routes app-server events by thread. For the active thread, handle_thread_event_now forwards notifications to ChatWidget::handle_server_notification. That handler maps turn start to running state, turn completion to final cleanup, item notifications to item-specific lifecycle handlers, and deltas to streaming or active command cells.

4.1 Streaming Is a Temporary Tail

handle_streaming_delta creates or updates a stream controller and starts commit animation. When the final assistant item lands, flush_answer_stream_with_separator consolidates the streaming cells into one source-backed markdown cell so resize and replay remain stable.

4.2 Item Type Chooses the History Cell

handle_item_started_notification dispatches by ThreadItem: commands to command lifecycle, patches to patch cells, MCP calls to MCP cells, web search to active search cells, and so on. Completed items pass through handle_thread_item, which is shared by live handling and replay.

4.3 Turn Completion Finalizes Temporary UI State

on_task_started resets turn flags, opens the running status, and shows the interrupt hint. on_task_complete flushes streaming and unified exec state, adds final separators when needed, clears running commands, and decides whether to send notifications or queued follow-up input.

5. Rollout Keeps Recovery Evidence

The rollout policy branches on history mode. Legacy stores older user-message, assistant-message, reasoning, and selected tool-completion events; paginated history primarily stores the complete TurnItem inside ItemCompleted. Both retain turn start, completion, interruption, usage, and settings-applied events. Intermediate deltas, approval requests, ItemStarted, and hook lifecycle events remain transient.

The RolloutRecorder first queues canonical items. persist materializes a deferred file; flush waits for queued writes. Sending a completion notification and confirming the writer has flushed are separate operations. A received turn/completed alone does not establish that every record is on disk. Loading parses JSONL records into InitialHistory::Resumed; core reconstructs surviving history and TUI replay renders safe items without live-only side effects.

For one assistant message, live deltas update a temporary TUI cell; completion produces a canonical item; paginated rollout stores that completed item; resume rebuilds the message without replaying every typing fragment. Historical ThreadRolledBack markers still require compatibility handling, but do not imply that the current app-server exposes a thread/rollback request.

6. Four Reading Rules

Visible symptom Ask first Source layer
Text is streaming. Is this a delta or a completed item? AgentMessageDelta and stream consolidation.
A command card changes. Is this lifecycle or output delta? event mapping and command lifecycle.
An approval prompt opens. Does the client need to answer? ServerRequestPayload.
History reappears after resume. Is this reconstructed state or a live event? rollout reconstruction and TUI replay.

These conversions keep the UI readable and history recoverable. Core produces facts, app-server emits protocol messages, TUI builds the live terminal experience, and rollout stores what resume needs. The next runtime layers, including plugins, MCP, skills, and subagents, still have to pass through the same question: who owns this fact, what can clients see, and what must survive for a future resume?

Sources