Codex Handbook
tui/src/chatwidget/streaming.rs 473 lines
//! Streaming transcript updates for `ChatWidget`.//!//! This module owns assistant, plan, and reasoning deltas, including stream-tail//! cells, commit ticks, and interrupt deferral.use super::*;impl ChatWidget {    pub(super) fn restore_reasoning_status_header(&mut self) {        if let Some(header) = extract_first_bold(&self.reasoning_buffer) {            self.status_state.terminal_title_status_kind = TerminalTitleStatusKind::Thinking;            self.set_status_header(header);        } else if self.bottom_pane.is_task_running() {            self.status_state.terminal_title_status_kind = TerminalTitleStatusKind::Working;            self.set_status_header(String::from("Working"));        }    }    pub(super) fn flush_answer_stream_with_separator(&mut self) {        let had_stream_controller = self.stream_controller.is_some();        if let Some(mut controller) = self.stream_controller.take() {            let scrollback_reflow = if controller.has_live_tail() {                crate::app_event::ConsolidationScrollbackReflow::Required            } else {                crate::app_event::ConsolidationScrollbackReflow::IfResizeReflowRan            };            self.clear_active_stream_tail();            let (cell, source) = controller.finalize();            let deferred_history_cell =                if scrollback_reflow == crate::app_event::ConsolidationScrollbackReflow::Required {                    cell                } else {                    if let Some(cell) = cell {                        self.add_boxed_history(cell);                    }                    None                };            // Consolidate the run of streaming AgentMessageCells into a single AgentMarkdownCell            // that can re-render from source on resize.            if let Some(source) = source {                let source =                    parse_assistant_markdown(&source, self.config.cwd.as_path()).visible_markdown;                self.note_stream_consolidation_queued();                self.app_event_tx.send(AppEvent::ConsolidateAgentMessage {                    source,                    cwd: self.config.cwd.to_path_buf(),                    scrollback_reflow,                    deferred_history_cell,                });            }        }        self.adaptive_chunking.reset();        if had_stream_controller && self.stream_controllers_idle() {            self.app_event_tx.send(AppEvent::StopCommitAnimation);        }        if had_stream_controller {            self.request_pending_usage_output_insertion_after_stream_shutdown();        }    }    pub(super) fn stream_controllers_idle(&self) -> bool {        self.stream_controller            .as_ref()            .map(|controller| controller.queued_lines() == 0)            .unwrap_or(true)            && self                .plan_stream_controller                .as_ref()                .map(|controller| controller.queued_lines() == 0)                .unwrap_or(true)    }    /// Restore the status indicator only after commentary completion is pending,    /// the turn is still running, and all stream queues have drained.    ///    /// This gate prevents flicker while normal output is still actively    /// streaming, but still restores a visible "working" affordance when a    /// commentary block ends before the turn itself has completed.    pub(super) fn maybe_restore_status_indicator_after_stream_idle(&mut self) {        if !self.status_state.pending_status_indicator_restore            || !self.bottom_pane.is_task_running()            || !self.stream_controllers_idle()        {            return;        }        self.bottom_pane.ensure_status_indicator();        self.set_status(            self.status_state.current_status.header.clone(),            self.status_state.current_status.details.clone(),            StatusDetailsCapitalization::Preserve,            self.status_state.current_status.details_max_lines,        );        self.status_state.pending_status_indicator_restore = false;    }    pub(super) fn finalize_completed_assistant_message(&mut self, message: Option<&str>) {        // If we have a stream_controller, the finalized message payload is redundant because the        // visible content has already been accumulated through deltas.        if self.stream_controller.is_none()            && let Some(message) = message            && !message.is_empty()        {            self.handle_streaming_delta(message.to_string());        }        self.flush_answer_stream_with_separator();        self.handle_stream_finished();        self.request_redraw();    }    pub(super) fn on_agent_message_delta(&mut self, delta: String) {        self.handle_streaming_delta(delta);    }    pub(super) fn on_plan_delta(&mut self, delta: String) {        if self.active_mode_kind() != ModeKind::Plan {            return;        }        if !delta.is_empty() {            self.record_visible_turn_activity();        }        if !self.transcript.plan_item_active {            self.transcript.plan_item_active = true;            self.transcript.plan_delta_buffer.clear();        }        self.transcript.plan_delta_buffer.push_str(&delta);        if self.plan_stream_controller.is_none() {            // Before starting a plan stream, flush any active exec cell group.            self.flush_unified_exec_wait_streak();            self.flush_active_cell();            self.plan_stream_controller = Some(PlanStreamController::new(                self.current_stream_width(/*reserved_cols*/ 4),                &self.config.cwd,                self.history_render_mode(),            ));        }        if let Some(controller) = self.plan_stream_controller.as_mut()            && controller.push(&delta)        {            self.app_event_tx.send(AppEvent::StartCommitAnimation);            self.run_catch_up_commit_tick();        }        self.sync_active_stream_tail();        self.request_redraw();    }    pub(super) fn on_plan_item_completed(&mut self, text: String) {        let streamed_plan = self.transcript.plan_delta_buffer.trim().to_string();        let plan_text = if text.trim().is_empty() {            streamed_plan        } else {            text        };        if !plan_text.trim().is_empty() {            self.record_agent_markdown(&plan_text);            self.transcript.latest_proposed_plan_markdown = Some(plan_text.clone());        }        // Plan commit ticks can hide the status row; remember whether we streamed plan output so        // completion can restore it once stream queues are idle.        let should_restore_after_stream = self.plan_stream_controller.is_some();        self.transcript.plan_delta_buffer.clear();        self.transcript.plan_item_active = false;        self.transcript.saw_plan_item_this_turn = true;        let (finalized_streamed_cell, consolidated_plan_source) =            if let Some(mut controller) = self.plan_stream_controller.take() {                let had_live_tail = controller.has_live_tail();                self.clear_active_stream_tail();                let (cell, source) = controller.finalize();                if had_live_tail {                    (None, source)                } else {                    (cell, source)                }            } else {                (None, None)            };        if let Some(cell) = finalized_streamed_cell {            self.add_boxed_history(cell);            // TODO: Replace streamed output with the final plan item text if plan streaming is            // removed or if we need to reconcile mismatches between streamed and final content.            if let Some(source) = consolidated_plan_source {                self.note_stream_consolidation_queued();                self.app_event_tx                    .send(AppEvent::ConsolidateProposedPlan(source));            }        } else if !plan_text.is_empty() {            self.add_to_history(history_cell::new_proposed_plan(plan_text, &self.config.cwd));        } else if let Some(source) = consolidated_plan_source {            self.note_stream_consolidation_queued();            self.app_event_tx                .send(AppEvent::ConsolidateProposedPlan(source));        }        if should_restore_after_stream {            self.status_state.pending_status_indicator_restore = true;            self.maybe_restore_status_indicator_after_stream_idle();            self.request_pending_usage_output_insertion_after_stream_shutdown();        }    }    pub(super) fn on_agent_reasoning_delta(&mut self, delta: String) {        // For reasoning deltas, do not stream to history. Accumulate the        // current reasoning block and extract the first bold element        // (between **/**) as the chunk header. Show this header as status.        self.reasoning_buffer.push_str(&delta);        if self.unified_exec_wait_streak.is_some() {            // Unified exec waiting should take precedence over reasoning-derived status headers.            self.request_redraw();            return;        }        if let Some(header) = extract_first_bold(&self.reasoning_buffer) {            // Update the shimmer header to the extracted reasoning chunk header.            self.status_state.terminal_title_status_kind = TerminalTitleStatusKind::Thinking;            self.set_status_header(header);        } else {            // Fallback while we don't yet have a bold header: leave existing header as-is.        }        self.request_redraw();    }    pub(super) fn on_agent_reasoning_final(&mut self) {        // At the end of a reasoning block, record transcript-only content.        self.full_reasoning_buffer.push_str(&self.reasoning_buffer);        if !self.full_reasoning_buffer.is_empty() {            let cell = history_cell::new_reasoning_summary_block(                self.full_reasoning_buffer.clone(),                &self.config.cwd,            );            self.add_boxed_history(cell);        }        self.reasoning_buffer.clear();        self.full_reasoning_buffer.clear();        self.request_redraw();    }    pub(super) fn on_reasoning_section_break(&mut self) {        // Start a new reasoning block for header extraction and accumulate transcript.        self.full_reasoning_buffer.push_str(&self.reasoning_buffer);        self.full_reasoning_buffer.push_str("\n\n");        self.reasoning_buffer.clear();    }    pub(super) fn on_stream_error(&mut self, message: String, additional_details: Option<String>) {        self.status_state.remember_retry_status_header();        self.bottom_pane.ensure_status_indicator();        self.status_state.terminal_title_status_kind = TerminalTitleStatusKind::Thinking;        self.set_status(            message,            additional_details,            StatusDetailsCapitalization::CapitalizeFirst,            STATUS_DETAILS_DEFAULT_MAX_LINES,        );    }    /// Handle completion of an `AgentMessage` turn item.    ///    /// Commentary completion sets a deferred restore flag so the status row    /// returns once stream queues are idle. Final-answer completion (or absent    /// phase for legacy models) clears the flag to preserve historical behavior.    pub(super) fn on_agent_message_item_completed(        &mut self,        item: AgentMessageItem,        from_replay: bool,    ) {        let mut message = String::new();        for content in &item.content {            match content {                AgentMessageContent::Text { text } => message.push_str(text),            }        }        let parsed = parse_assistant_markdown(&message, self.config.cwd.as_path());        self.finalize_completed_assistant_message(            (!parsed.visible_markdown.is_empty()).then_some(parsed.visible_markdown.as_str()),        );        if matches!(item.phase, Some(MessagePhase::FinalAnswer) | None)            && !parsed.visible_markdown.is_empty()        {            self.record_agent_markdown(&parsed.visible_markdown);        }        if !from_replay            && let Some(cwd) = parsed.last_created_branch_cwd()            && let Some(thread_id) = self.thread_id            && let Some(runner) = self.workspace_command_runner.clone()        {            let cwd = PathBuf::from(cwd);            let tx = self.app_event_tx.clone();            tokio::spawn(async move {                if let Some(branch) =                    crate::branch_summary::current_branch_name(runner.as_ref(), &cwd).await                {                    tx.send(AppEvent::SyncThreadGitBranch { thread_id, branch });                }            });        }        self.status_state.pending_status_indicator_restore = match item.phase {            // Models that don't support preambles only output AgentMessageItems on turn completion.            Some(MessagePhase::FinalAnswer) | None => !self.input_queue.pending_steers.is_empty(),            Some(MessagePhase::Commentary) => true,        };        self.maybe_restore_status_indicator_after_stream_idle();    }    /// Periodic tick for stream commits. In smooth mode this preserves one-line pacing, while    /// catch-up mode drains larger batches to reduce queue lag.    pub(crate) fn on_commit_tick(&mut self) {        self.run_commit_tick();    }    /// Runs a regular periodic commit tick.    pub(super) fn run_commit_tick(&mut self) {        self.run_commit_tick_with_scope(CommitTickScope::AnyMode);    }    /// Runs an opportunistic commit tick only if catch-up mode is active.    pub(super) fn run_catch_up_commit_tick(&mut self) {        self.run_commit_tick_with_scope(CommitTickScope::CatchUpOnly);    }    /// Runs a commit tick for the current stream queue snapshot.    ///    /// `scope` controls whether this call may commit in smooth mode or only when catch-up    /// is currently active. While lines are actively streaming we hide the status row to avoid    /// duplicate "in progress" affordances. Restoration is gated separately so we only re-show    /// the row after commentary completion once stream queues are idle.    pub(super) fn run_commit_tick_with_scope(&mut self, scope: CommitTickScope) {        let now = Instant::now();        let outcome = run_commit_tick(            &mut self.adaptive_chunking,            self.stream_controller.as_mut(),            self.plan_stream_controller.as_mut(),            scope,            now,        );        for cell in outcome.cells {            self.bottom_pane.hide_status_indicator();            self.add_boxed_history(cell);        }        self.sync_active_stream_tail();        if outcome.has_controller && outcome.all_idle {            self.maybe_restore_status_indicator_after_stream_idle();            self.app_event_tx.send(AppEvent::StopCommitAnimation);        }        if self.turn_lifecycle.agent_turn_running {            self.refresh_runtime_metrics();        }    }    pub(super) fn flush_interrupt_queue(&mut self) {        let mut mgr = std::mem::take(&mut self.interrupts);        mgr.flush_all(self);        self.interrupts = mgr;    }    #[inline]    pub(super) fn defer_or_handle(        &mut self,        push: impl FnOnce(&mut InterruptManager),        handle: impl FnOnce(&mut Self),    ) {        // Preserve deterministic FIFO across queued interrupts: once anything        // is queued due to an active write cycle, continue queueing until the        // queue is flushed to avoid reordering (e.g., ExecEnd before ExecBegin).        if self.stream_controller.is_some() || !self.interrupts.is_empty() {            push(&mut self.interrupts);        } else {            handle(self);        }    }    pub(super) fn handle_stream_finished(&mut self) {        if self.task_complete_pending {            self.bottom_pane.hide_status_indicator();            self.task_complete_pending = false;        }        // A completed stream indicates non-exec content was just inserted.        self.flush_interrupt_queue();    }    #[inline]    pub(super) fn handle_streaming_delta(&mut self, delta: String) {        if !delta.is_empty() {            self.record_visible_turn_activity();        }        if self.stream_controller.is_none() {            // Before starting an agent stream, flush any active exec cell group.            self.flush_unified_exec_wait_streak();            self.flush_active_cell();            // If the previous turn inserted non-stream history (exec output, patch status, MCP            // calls), render a separator before starting the next streamed assistant message.            if self.transcript.needs_final_message_separator && self.transcript.had_work_activity {                self.add_to_history(history_cell::FinalMessageSeparator::new(                    /*elapsed_seconds*/ None, /*runtime_metrics*/ None,                ));                self.transcript.needs_final_message_separator = false;            } else if self.transcript.needs_final_message_separator {                // Reset the flag even if we don't show separator (no work was done)                self.transcript.needs_final_message_separator = false;            }            self.stream_controller = Some(StreamController::new(                self.current_stream_width(/*reserved_cols*/ 2),                &self.config.cwd,                self.history_render_mode(),            ));        }        if let Some(controller) = self.stream_controller.as_mut()            && controller.push(&delta)        {            self.app_event_tx.send(AppEvent::StartCommitAnimation);            self.run_catch_up_commit_tick();        }        self.sync_active_stream_tail();        self.request_redraw();    }    pub(super) fn active_cell_is_stream_tail(&self) -> bool {        self.transcript.active_cell.as_ref().is_some_and(|cell| {            cell.as_any().is::<history_cell::StreamingAgentTailCell>()                || cell.as_any().is::<history_cell::StreamingPlanTailCell>()        })    }    pub(super) fn has_active_stream_tail(&self) -> bool {        (self.stream_controller.is_some() || self.plan_stream_controller.is_some())            && self.active_cell_is_stream_tail()    }    pub(super) fn sync_active_stream_tail(&mut self) {        if let Some(controller) = self.stream_controller.as_ref() {            let tail_lines = controller.current_tail_lines();            if tail_lines.is_empty() {                self.clear_active_stream_tail();                return;            }            self.bottom_pane.hide_status_indicator();            self.transcript.active_cell =                Some(Box::new(history_cell::StreamingAgentTailCell::new(                    tail_lines,                    controller.tail_starts_stream(),                )));            self.bump_active_cell_revision();            return;        }        if let Some(controller) = self.plan_stream_controller.as_ref() {            let tail_lines = controller.current_tail_display_lines();            if tail_lines.is_empty() {                self.clear_active_stream_tail();                return;            }            self.bottom_pane.hide_status_indicator();            self.transcript.active_cell = Some(Box::new(history_cell::StreamingPlanTailCell::new(                tail_lines,                !controller.tail_starts_stream(),            )));            self.bump_active_cell_revision();            return;        }        self.clear_active_stream_tail();    }    pub(super) fn clear_active_stream_tail(&mut self) {        if self.active_cell_is_stream_tail() {            self.transcript.active_cell = None;            self.bump_active_cell_revision();        }    }}