Codex Handbook
tui/src/chatwidget/replay.rs 203 lines
//! Thread replay rendering for `ChatWidget`.//!//! This module rehydrates turns and items into transcript state while avoiding//! live-only side effects.use super::*;impl ChatWidget {    /// Replay a subset of initial events into the UI to seed the transcript when    /// resuming an existing session. This approximates the live event flow and    /// is intentionally conservative: only safe-to-replay items are rendered to    /// avoid triggering side effects. Event ids are passed as `None` to    /// distinguish replayed events from live ones.    pub(crate) fn replay_thread_turns(&mut self, turns: Vec<Turn>, replay_kind: ReplayKind) {        for turn in turns {            let Turn {                id: turn_id,                items_view: _,                items,                status,                error,                started_at,                completed_at,                duration_ms,            } = turn;            if matches!(status, TurnStatus::InProgress) {                self.last_non_retry_error = None;                self.on_task_started();            }            for item in items {                self.replay_thread_item(item, turn_id.clone(), replay_kind);            }            if matches!(                status,                TurnStatus::Completed | TurnStatus::Interrupted | TurnStatus::Failed            ) {                self.handle_turn_completed_notification(                    TurnCompletedNotification {                        thread_id: self.thread_id.map(|id| id.to_string()).unwrap_or_default(),                        turn: Turn {                            id: turn_id,                            items_view: codex_app_server_protocol::TurnItemsView::NotLoaded,                            items: Vec::new(),                            status,                            error,                            started_at,                            completed_at,                            duration_ms,                        },                    },                    Some(replay_kind),                );            }        }    }    pub(crate) fn replay_thread_item(        &mut self,        item: ThreadItem,        turn_id: String,        replay_kind: ReplayKind,    ) {        self.handle_thread_item(item, turn_id, ThreadItemRenderSource::Replay(replay_kind));    }    pub(super) fn handle_thread_item(        &mut self,        item: ThreadItem,        turn_id: String,        render_source: ThreadItemRenderSource,    ) {        let from_replay = render_source.is_replay();        let replay_kind = render_source.replay_kind();        match item {            ThreadItem::UserMessage { content, .. } => {                self.on_committed_user_message(&content, from_replay);            }            ThreadItem::AgentMessage {                id,                text,                phase,                memory_citation,            } => {                self.on_agent_message_item_completed(                    AgentMessageItem {                        id,                        content: vec![AgentMessageContent::Text { text }],                        phase,                        memory_citation: memory_citation.map(|citation| {                            codex_protocol::memory_citation::MemoryCitation {                                entries: citation                                    .entries                                    .into_iter()                                    .map(|entry| {                                        codex_protocol::memory_citation::MemoryCitationEntry {                                            path: entry.path,                                            line_start: entry.line_start,                                            line_end: entry.line_end,                                            note: entry.note,                                        }                                    })                                    .collect(),                                rollout_ids: citation.thread_ids,                            }                        }),                    },                    from_replay,                );            }            ThreadItem::Plan { text, .. } => self.on_plan_item_completed(text),            ThreadItem::Reasoning {                summary, content, ..            } => {                if from_replay {                    for delta in summary {                        self.on_agent_reasoning_delta(delta);                    }                    if self.config.show_raw_agent_reasoning {                        for delta in content {                            self.on_agent_reasoning_delta(delta);                        }                    }                }                self.on_agent_reasoning_final();            }            item @ ThreadItem::CommandExecution {                status: codex_app_server_protocol::CommandExecutionStatus::InProgress,                ..            } => self.on_command_execution_started(item),            item @ ThreadItem::CommandExecution { .. } => self.on_command_execution_completed(item),            ThreadItem::FileChange {                status: codex_app_server_protocol::PatchApplyStatus::InProgress,                ..            } => {}            item @ ThreadItem::FileChange { .. } => self.on_file_change_completed(item),            item @ ThreadItem::McpToolCall {                status: codex_app_server_protocol::McpToolCallStatus::InProgress,                ..            } => self.on_mcp_tool_call_started(item),            item @ ThreadItem::McpToolCall { .. } => self.on_mcp_tool_call_completed(item),            ThreadItem::WebSearch { id, query, action } => {                self.on_web_search_begin(id.clone());                self.on_web_search_end(                    id,                    query,                    action.unwrap_or(codex_app_server_protocol::WebSearchAction::Other),                );            }            ThreadItem::ImageView { id: _, path } => {                self.on_view_image_tool_call(path);            }            ThreadItem::ImageGeneration {                id,                status,                revised_prompt,                saved_path,                ..            } => {                self.on_image_generation_end(id, status, revised_prompt, saved_path);            }            ThreadItem::EnteredReviewMode { review, .. } => {                if from_replay {                    self.enter_review_mode_with_hint(review, /*from_replay*/ true);                }            }            ThreadItem::ExitedReviewMode { .. } => {                self.exit_review_mode_after_item();            }            ThreadItem::ContextCompaction { .. } => {                self.add_info_message("Context compacted".to_string(), /*hint*/ None);            }            ThreadItem::HookPrompt { .. } => {}            ThreadItem::CollabAgentToolCall {                id,                tool,                status,                sender_thread_id,                receiver_thread_ids,                prompt,                model,                reasoning_effort,                agents_states,            } => self.on_collab_agent_tool_call(ThreadItem::CollabAgentToolCall {                id,                tool,                status,                sender_thread_id,                receiver_thread_ids,                prompt,                model,                reasoning_effort,                agents_states,            }),            item @ ThreadItem::SubAgentActivity { .. } => self.on_sub_agent_activity(item),            ThreadItem::DynamicToolCall { .. } => {}            ThreadItem::Sleep { .. } => {}        }        if matches!(replay_kind, Some(ReplayKind::ThreadSnapshot)) && turn_id.is_empty() {            self.request_redraw();        }    }}