use super::provider_context::provider_context_injection_transcript_row;
use super::*;
impl MissionControlState {
pub(crate) fn apply_output_event(&mut self, event: &OutputEvent) {
match event {
OutputEvent::SessionHeader { model, cwd, .. } => {
self.finish_hard_stream_boundary();
let model = transcript::sanitize_preview(model);
let cwd = transcript::sanitize_preview(&cwd.display().to_string());
self.push_transcript(format!(
"session: {model}{PADDED_INLINE_SEPARATOR}{}",
crate::tui::normalize_inline_separators(&cwd)
))
}
OutputEvent::UserPrompt { text } => {
self.finish_hard_stream_boundary();
self.context_usage = None;
let text = transcript::sanitize_preview(text);
self.push_transcript(format!("you: {text}"));
}
OutputEvent::BashCommand { command } => {
self.finish_hard_stream_boundary();
self.context_usage = None;
let command = transcript::sanitize_preview(command);
self.push_transcript(format!("bash: {command}"));
}
OutputEvent::ContextUsage {
current_tokens,
max_tokens,
reasoning_tokens,
source,
request_sequence,
} => {
self.update_context_usage_from_event(
*current_tokens,
*max_tokens,
*reasoning_tokens,
*source,
*request_sequence,
);
}
OutputEvent::ThinkingSummaryDelta { text } => {
let changed_index = self.active_assistant_transcript_index();
self.assistant_streaming = false;
self.invalidate_transcript_changed_entry_cache(0, changed_index);
self.append_thinking_delta(text);
}
OutputEvent::ThinkingSummaryComplete { text } => {
let changed_index = self.active_assistant_transcript_index();
self.assistant_streaming = false;
self.invalidate_transcript_changed_entry_cache(0, changed_index);
self.reconcile_thinking_summary(text, None, None);
}
OutputEvent::ThinkingSummaryCompleteIdentified {
text,
item_id,
turn_id,
} => {
let changed_index = self.active_assistant_transcript_index();
self.assistant_streaming = false;
self.invalidate_transcript_changed_entry_cache(0, changed_index);
self.reconcile_thinking_summary(text, item_id.as_deref(), turn_id.as_deref());
}
OutputEvent::AssistantDelta { text } => {
self.finish_thinking_live_redaction();
self.assistant_streaming = true;
self.append_assistant_delta(text);
}
OutputEvent::AssistantComplete { text } => {
self.assistant_streaming = false;
let _ = self.finish_assistant_live_redaction();
self.reconcile_assistant_text(text);
}
OutputEvent::Diagnostic { level, message } => {
self.finish_hard_stream_boundary();
self.push_transcript(format!(
"{}: {}",
level,
crate::output::redact_sensitive_text(message)
));
}
OutputEvent::HookDiagnostic { diagnostic } => {
self.finish_hard_stream_boundary();
self.push_transcript(format!("hook: {}", diagnostic.sanitized_message()));
}
OutputEvent::ProviderContextInjection { metadata } => {
self.finish_hard_stream_boundary();
self.push_transcript(provider_context_injection_transcript_row(metadata));
}
OutputEvent::SubdirInstructionInjection { path, .. } => {
self.finish_hard_stream_boundary();
self.push_transcript(format!(
"Loaded subdirectory instructions: {}",
crate::output::redact_sensitive_text(&path.display().to_string())
));
}
OutputEvent::ToolStarted { call, label } => {
self.finish_hard_stream_boundary();
if !call.id.trim().is_empty() {
self.start_tool_transcript(call, label);
}
}
OutputEvent::ToolResult {
call,
result,
summary,
} => {
self.finish_hard_stream_boundary();
self.finish_tool_transcript(
ActivityId::new(call.id.clone()),
call.name.clone(),
transcript_tool_row(call, summary),
);
if !call.id.trim().is_empty() {
let preview = format_tool_activity_detail_block(call, result);
self.upsert_tool_result_preview(
ActivityId::new(call.id.clone()),
&preview,
ActivityMetadata {
label: summary.label.clone(),
detail: Some(activity::final_preview_detail(&summary.tool_name)),
fields: summary.metadata.clone(),
},
if result.success {
ActivityStatus::Success
} else {
ActivityStatus::Failed
},
);
}
}
}
}
pub(crate) fn apply_session_title_update(&mut self, session_id: &str, title: &str) {
let label = format!("{} [{}]", title, short_session_id(session_id));
if self.footer_session_id.as_deref() == Some(session_id) {
self.footer_session = label.clone();
}
if let Some(picker) = &mut self.modals.session_picker
&& let Some(row) = picker.rows.iter_mut().find(|row| row.id == session_id)
{
row.label = label;
}
}
pub(crate) fn finish_assistant_streaming(&mut self) {
if self.assistant_streaming {
let changed_index = self.active_assistant_transcript_index;
let visible_tail = self.finish_assistant_live_redaction();
self.append_assistant_visible_delta(&visible_tail);
self.assistant_streaming = false;
self.invalidate_transcript_changed_entry_cache(0, changed_index);
}
}
pub(crate) fn record_error_transcript(&mut self, message: &str) {
if is_transient_status_error(message) {
return;
}
self.finish_hard_stream_boundary();
let before = self.transcript.len();
let last = self.transcript.back().cloned();
let drained = transcript::push_error_transcript(&mut self.transcript, message);
self.normalize_transcript_indices_after_drain(drained);
if self.transcript.len() != before || self.transcript.back() != last.as_ref() {
self.invalidate_transcript_append_cache(drained);
}
}
pub(crate) fn record_canceled_transcript(&mut self, prompt_preview: &str) {
self.finish_hard_stream_boundary();
let preview = transcript::sanitize_preview(prompt_preview);
let line = if preview.trim().is_empty() {
"canceled: prompt".to_string()
} else {
format!("canceled: {preview}")
};
if self.transcript.back().is_some_and(|last| last == &line) {
return;
}
self.push_transcript(line);
}
pub(crate) fn record_compaction_started_transcript(&mut self) {
self.finish_hard_stream_boundary();
let line = "compact: started".to_string();
if self.transcript.back().is_some_and(|last| last == &line) {
return;
}
self.push_transcript(line);
}
pub(crate) fn record_compaction_complete_transcript(&mut self, summary: Option<&str>) {
self.finish_hard_stream_boundary();
let line = summary
.map(crate::output::redact_sensitive_text)
.map(|summary| transcript::sanitize_preview(&summary))
.filter(|summary| !summary.trim().is_empty())
.map(|summary| format!("compact: complete{PADDED_INLINE_SEPARATOR}{summary}"))
.unwrap_or_else(|| "compact: complete".to_string());
if self.transcript.back().is_some_and(|last| last == &line) {
return;
}
self.push_transcript(line);
}
fn update_context_usage_from_event(
&mut self,
current_tokens: usize,
max_tokens: usize,
reasoning_tokens: Option<usize>,
source: ContextUsageSource,
request_sequence: u64,
) {
let incoming = ContextUsageState {
current_tokens,
max_tokens,
reasoning_tokens,
source,
request_sequence,
};
self.context_usage = Some(merge_context_usage(self.context_usage, incoming));
}
pub(super) fn push_transcript(&mut self, line: String) {
let drained = transcript::push_transcript(&mut self.transcript, line);
self.normalize_transcript_indices_after_drain(drained);
self.retain_valid_active_tool_transcripts();
self.invalidate_transcript_append_cache(drained);
}
fn start_tool_transcript(&mut self, call: &crate::providers::ToolCall, label: &str) {
let id = ActivityId::new(call.id.clone());
let label = normalize_inline_separators(&transcript::sanitize_preview(label));
let activity = pending_tool_display_summary(call)
.map(|summary| {
let parts = transcript_tool_metadata(&summary);
let status = crate::output::tool_status_label(summary.status);
if parts.is_empty() {
status.to_string()
} else {
format!(
"{status}{PADDED_INLINE_SEPARATOR}{}",
parts.join(PADDED_INLINE_SEPARATOR)
)
}
})
.unwrap_or_else(|| "starting".to_string());
let subagents = (call.name == "subagents")
.then(|| ActiveSubagentTranscript::from_tool_call(&id, &call.arguments));
let active = ActiveToolTranscript {
index: self.transcript.len(),
label,
activity,
subagents,
};
let drained = transcript::push_transcript(&mut self.transcript, active.render_line());
self.normalize_transcript_indices_after_drain(drained);
let index = active.index.saturating_sub(drained);
let absolute_index = self
.transcript_absolute_index(index)
.unwrap_or(self.transcript_index_base);
let mut active = active;
active.index = absolute_index;
self.transcript_activity_links.insert(
absolute_index,
TranscriptActivityLink {
activity_id: id.clone(),
tool_name: call.name.clone(),
},
);
self.active_tool_transcripts.insert(id, active);
self.invalidate_transcript_append_cache(drained);
}
pub(super) fn update_tool_transcript(&mut self, id: &ActivityId, activity: &str) {
let Some(mut active) = self.active_tool_transcripts.remove(id) else {
return;
};
active.activity = normalize_inline_separators(&transcript::sanitize_preview(activity));
let Some(index) = self.transcript_local_index(active.index) else {
return;
};
let line = active.render_line();
self.active_tool_transcripts.insert(id.clone(), active);
if let Some(transcript_line) = self.transcript.get_mut(index) {
*transcript_line = line;
self.invalidate_transcript_entry_cache(index);
}
}
fn finish_tool_transcript(&mut self, id: ActivityId, tool_name: String, final_row: String) {
if let Some(active) = self.active_tool_transcripts.remove(&id)
&& let Some(index) = self.transcript_local_index(active.index)
&& let Some(line) = self.transcript.get_mut(index)
{
*line = normalize_inline_separators(&transcript::sanitize_preview(&format!(
"tool: {final_row}"
)));
self.invalidate_transcript_entry_cache(index);
return;
}
let index = self.transcript.len();
self.push_transcript(normalize_inline_separators(&format!("tool: {final_row}")));
if !id.as_str().trim().is_empty() {
let normalized_index = index.min(self.transcript.len().saturating_sub(1));
let absolute_index = self
.transcript_absolute_index(normalized_index)
.unwrap_or(self.transcript_index_base);
self.transcript_activity_links.insert(
absolute_index,
TranscriptActivityLink {
activity_id: id,
tool_name,
},
);
self.invalidate_transcript_visual_projection_cache();
}
}
pub(super) fn retain_valid_active_tool_transcripts(&mut self) {
let active_start = self.transcript_index_base;
let active_end = active_start.saturating_add(self.transcript.len());
self.active_tool_transcripts
.retain(|_, active| (active_start..active_end).contains(&active.index));
self.transcript_activity_links
.retain(|index, _| (active_start..active_end).contains(index));
}
fn normalize_transcript_indices_after_drain(&mut self, drained: usize) {
if drained == 0 {
return;
}
let old_base = self.transcript_index_base;
self.transcript_index_base = old_base.saturating_add(drained);
self.retain_valid_active_tool_transcripts();
self.active_assistant_transcript_index = self
.active_assistant_transcript_index
.and_then(|index| index.checked_sub(drained));
self.active_thinking_transcript_index = self
.active_thinking_transcript_index
.and_then(|index| index.checked_sub(drained));
self.thinking_transcript_identities = self
.thinking_transcript_identities
.drain()
.filter_map(|(id, index)| index.checked_sub(drained).map(|index| (id, index)))
.collect();
self.invalidate_transcript_front_cache(old_base, drained);
}
fn finish_assistant_live_redaction(&mut self) -> String {
self.active_assistant_text.clear();
let visible_tail = self.active_assistant_redactor.flush();
self.active_assistant_redactor = StreamingRedactor::default();
visible_tail
}
fn finish_thinking_live_redaction(&mut self) {
let visible_tail = self.active_thinking_redactor.flush();
let visible_tail = self.active_thinking_control_sanitizer.push(&visible_tail);
self.append_thinking_visible_delta(&visible_tail);
self.active_thinking_text.clear();
self.active_thinking_redactor = StreamingRedactor::default();
self.active_thinking_control_sanitizer.reset();
}
fn finish_hard_stream_boundary(&mut self) {
let changed_index = self.active_assistant_transcript_index();
let visible_tail = self.finish_assistant_live_redaction();
self.append_assistant_visible_delta(&visible_tail);
self.finish_thinking_live_redaction();
self.active_assistant_transcript_index = None;
self.active_thinking_transcript_index = None;
self.thinking_transcript_identities.clear();
self.assistant_streaming = false;
self.invalidate_transcript_changed_entry_cache(0, changed_index);
}
fn append_assistant_delta(&mut self, text: &str) {
if text.is_empty() || (self.active_assistant_text.is_empty() && text.trim().is_empty()) {
return;
}
if self.active_assistant_text.is_empty()
&& self.active_assistant_transcript_index.is_some()
&& transcript::latest_current_segment_entry_for_prefix(&self.transcript, "assistant: ")
.is_none()
{
self.active_assistant_transcript_index = None;
}
self.active_assistant_text.push_str(text);
let visible_delta = self.active_assistant_redactor.push(text);
if visible_delta.is_empty() {
self.ensure_active_assistant_row();
} else {
self.append_assistant_visible_delta(&visible_delta);
}
}
fn ensure_active_assistant_row(&mut self) {
if let Some(index) = self.active_assistant_transcript_index
&& self
.transcript
.get(index)
.is_some_and(|line| line.starts_with("assistant: "))
{
return;
}
self.active_assistant_transcript_index = None;
let index = self.transcript.len();
let drained = transcript::push_transcript(&mut self.transcript, "assistant: ".to_string());
self.normalize_transcript_indices_after_drain(drained);
self.active_assistant_transcript_index = Some(
index
.saturating_sub(drained)
.min(self.transcript.len().saturating_sub(1)),
);
self.retain_valid_active_tool_transcripts();
self.invalidate_transcript_append_cache(drained);
}
fn append_assistant_visible_delta(&mut self, visible_delta: &str) {
if visible_delta.is_empty() {
return;
}
let visible_delta = transcript::sanitize_preview(visible_delta);
if visible_delta.is_empty() {
return;
}
if let Some(index) = self.active_assistant_transcript_index
&& let Some(line) = self.transcript.get_mut(index)
&& line.starts_with("assistant: ")
{
line.push_str(&visible_delta);
self.invalidate_transcript_entry_cache(index);
return;
}
self.active_assistant_transcript_index = None;
let index = self.transcript.len();
let drained = transcript::push_transcript(
&mut self.transcript,
format!("assistant: {visible_delta}"),
);
self.normalize_transcript_indices_after_drain(drained);
self.active_assistant_transcript_index = Some(
index
.saturating_sub(drained)
.min(self.transcript.len().saturating_sub(1)),
);
self.retain_valid_active_tool_transcripts();
self.invalidate_transcript_append_cache(drained);
}
fn reconcile_assistant_text(&mut self, text: &str) {
if text.trim().is_empty() {
self.active_assistant_text.clear();
self.invalidate_transcript_changed_entry_cache(
0,
self.active_assistant_transcript_index,
);
return;
}
self.active_assistant_text.clear();
self.active_assistant_redactor = StreamingRedactor::default();
let (drained, index) = transcript::reconcile_assistant_text(
&mut self.transcript,
text,
self.active_assistant_transcript_index,
);
self.normalize_transcript_indices_after_drain(drained);
self.active_assistant_transcript_index = None;
self.invalidate_transcript_changed_entry_cache(drained, index);
self.mark_final_assistant_plain_first(index);
}
fn append_thinking_delta(&mut self, text: &str) {
if text.is_empty() {
return;
}
let visible_delta = self.active_thinking_redactor.push(text);
let visible_delta = self.active_thinking_control_sanitizer.push(&visible_delta);
self.append_thinking_visible_delta(&visible_delta);
}
fn append_thinking_visible_delta(&mut self, visible_delta: &str) {
if visible_delta.is_empty() {
return;
}
#[cfg(test)]
{
self.thinking_append_work_bytes = self
.thinking_append_work_bytes
.saturating_add(visible_delta.len());
}
let visible_delta = transcript::sanitize_preview(visible_delta);
if visible_delta.is_empty() {
return;
}
self.active_thinking_text.push_str(&visible_delta);
if let Some(index) = self.active_thinking_transcript_index
&& let Some(line) = self.transcript.get_mut(index)
&& line.starts_with("thinking: ")
{
line.push_str(&visible_delta);
self.invalidate_transcript_entry_cache(index);
return;
}
let index = self.transcript.len();
let drained =
transcript::push_transcript(&mut self.transcript, format!("thinking: {visible_delta}"));
self.normalize_transcript_indices_after_drain(drained);
self.active_thinking_transcript_index = Some(
index
.saturating_sub(drained)
.min(self.transcript.len().saturating_sub(1)),
);
self.retain_valid_active_tool_transcripts();
self.invalidate_transcript_append_cache(drained);
}
fn reconcile_thinking_summary(
&mut self,
text: &str,
identity: Option<&str>,
turn_id: Option<&str>,
) {
let active_thinking_text = std::mem::take(&mut self.active_thinking_text);
self.active_thinking_redactor = StreamingRedactor::default();
self.active_thinking_control_sanitizer.reset();
let text = transcript::sanitize_preview(text);
let text = transcript::StreamingControlSanitizer::default().push(&text);
if text.is_empty() {
self.active_thinking_transcript_index = None;
return;
}
let final_line = format!("thinking: {text}");
let identity_key = identity.map(|item| ThinkingTranscriptIdentity {
item_id: item.to_string(),
turn_id: turn_id.map(str::to_string),
});
if let Some(identity_key) = identity_key.as_ref()
&& let Some(index) = self
.thinking_transcript_identities
.get(identity_key)
.copied()
{
if self
.transcript
.get(index)
.is_some_and(|line| line.starts_with("thinking: "))
{
if let Some(line) = self.transcript.get_mut(index) {
*line = final_line;
}
self.active_thinking_transcript_index = None;
self.invalidate_transcript_entry_cache(index);
return;
}
self.thinking_transcript_identities.remove(identity_key);
}
if let Some(index) = self.active_thinking_transcript_index
&& let Some(line) = self.transcript.get_mut(index)
&& line.starts_with("thinking: ")
{
*line = final_line.clone();
if let Some(identity_key) = identity_key.as_ref() {
self.thinking_transcript_identities
.insert(identity_key.clone(), index);
}
self.active_thinking_transcript_index = None;
self.invalidate_transcript_entry_cache(index);
return;
}
if let Some(index) =
transcript::latest_current_segment_entry_for_prefix(&self.transcript, "thinking: ")
&& self.transcript.get(index).is_some_and(|line| {
line.starts_with("thinking: ")
&& !active_thinking_text.is_empty()
&& transcript::prefixed_line_matches(line, "thinking: ", &active_thinking_text)
})
{
if let Some(line) = self.transcript.get_mut(index) {
*line = final_line.clone();
}
if let Some(identity_key) = identity_key.as_ref() {
self.thinking_transcript_identities
.insert(identity_key.clone(), index);
}
self.active_thinking_transcript_index = None;
self.invalidate_transcript_entry_cache(index);
return;
}
let index = self.transcript.len();
let drained = transcript::push_transcript(&mut self.transcript, final_line);
self.normalize_transcript_indices_after_drain(drained);
let local_index = index
.saturating_sub(drained)
.min(self.transcript.len().saturating_sub(1));
if let Some(identity_key) = identity_key {
self.thinking_transcript_identities
.insert(identity_key, local_index);
}
self.active_thinking_transcript_index = None;
self.invalidate_transcript_append_cache(drained);
}
pub(super) fn upsert_activity_final_preview(
&mut self,
id: ActivityId,
preview: &str,
metadata: ActivityMetadata,
status: ActivityStatus,
) {
if !self.nodes.contains_key(&id) {
self.attach(&id, None);
self.nodes.insert(
id.clone(),
ActivityNode {
id: id.clone(),
parent_id: None,
kind: ActivityKind::Diagnostic,
status,
metadata: metadata.clone(),
started_at: chrono::Local::now(),
preview: String::new(),
preview_truncated: false,
},
);
}
if let Some(node) = self.nodes.get_mut(&id) {
node.status = status;
node.metadata = metadata;
node.preview.clear();
node.preview_truncated = false;
activity::append_preview(node, preview);
}
}
fn upsert_tool_result_preview(
&mut self,
id: ActivityId,
preview: &str,
metadata: ActivityMetadata,
status: ActivityStatus,
) {
if !self.nodes.contains_key(&id) {
self.attach(&id, None);
self.nodes.insert(
id.clone(),
ActivityNode {
id: id.clone(),
parent_id: None,
kind: ActivityKind::Tool,
status,
metadata: metadata.clone(),
started_at: chrono::Local::now(),
preview: String::new(),
preview_truncated: false,
},
);
}
if let Some(node) = self.nodes.get_mut(&id) {
node.status = status;
node.metadata = metadata;
node.preview.clear();
node.preview_truncated = false;
activity::append_preview(node, preview);
}
self.invalidate_activity_render_cache();
}
}