use super::provider_context::provider_context_injection_transcript_row;
use super::*;
use crate::output::{StreamingRedactor, pending_tool_display_summary};
impl MissionControlState {
pub(crate) fn apply_output_event_at(
&mut self,
event: &OutputEvent,
timestamp: Option<chrono::DateTime<chrono::Utc>>,
) {
let previous_timestamp = self.transcript_timestamp_context;
self.transcript_timestamp_context =
timestamp.map(|timestamp| timestamp.with_timezone(&chrono::Local));
self.apply_output_event_inner(event);
self.transcript_timestamp_context = previous_timestamp;
}
pub(crate) fn apply_output_event(&mut self, event: &OutputEvent) {
self.apply_output_event_at(event, None);
}
fn apply_output_event_inner(&mut self, event: &OutputEvent) {
match event {
OutputEvent::SessionHeader {
session_id,
model,
cwd,
} => {
if &self.session_files.session_id != session_id {
self.session_files = Default::default();
self.session_files.session_id = session_id.clone();
}
self.session_files.cwd = cwd.clone();
let header = LastSessionHeader {
session_id: session_id.clone(),
model: model.clone(),
};
if self.last_session_header.as_ref() == Some(&header) {
return;
}
self.last_session_header = Some(header);
self.finish_hard_stream_boundary();
let model = transcript::sanitize_preview(model);
let cwd = transcript::sanitize_preview(&cwd.display().to_string());
self.push_transcript(transcript::TranscriptEntry::session(format!(
"{model}{PADDED_INLINE_SEPARATOR}{}",
crate::tui::normalize_inline_separators(&cwd)
)));
}
OutputEvent::UserPrompt { text } => {
self.pending_compaction_transcript_index = None;
self.finish_hard_stream_boundary();
self.reset_context_for_new_prompt();
let text = transcript::sanitize_preview(text);
self.push_transcript(transcript::TranscriptEntry::user_prompt(&text));
}
OutputEvent::AutomaticUserPrompt { text } => {
self.pending_compaction_transcript_index = None;
self.finish_hard_stream_boundary();
self.reset_context_for_new_prompt();
let text = transcript::sanitize_preview(text);
self.push_transcript(transcript::TranscriptEntry::automatic_user_prompt(&text));
}
OutputEvent::CompactionTriggered {
current_tokens,
max_tokens,
threshold,
} => {
self.finish_hard_stream_boundary();
let entry = transcript::TranscriptEntry::compaction(format!(
"{current_tokens}/{max_tokens}{}{}",
PADDED_INLINE_SEPARATOR,
transcript::sanitize_preview(threshold)
));
self.push_transcript(entry);
let local_index = self.transcript.len().saturating_sub(1);
self.pending_compaction_transcript_index =
self.transcript_absolute_index(local_index);
}
OutputEvent::CompactionStarted => self.record_compaction_started_transcript(),
OutputEvent::CompactionCompleted {
current_tokens,
max_tokens,
summary,
} => {
self.finish_hard_stream_boundary();
self.context_usage = Some(ContextUsageState {
current_tokens: *current_tokens,
max_tokens: *max_tokens,
reasoning_tokens: None,
source: ContextUsageSource::FallbackEstimate,
request_sequence: 0,
});
self.record_compaction_complete_transcript(Some(summary));
}
OutputEvent::CompactionFailed { message, canceled } => {
self.finish_compaction_failure_transcript(message, *canceled);
}
OutputEvent::FastObservation {
provider_id,
model,
requested_service_tier,
outcome,
request_sequence,
run_order,
} => {
self.accept_fast_observation(
provider_id,
model,
requested_service_tier,
outcome,
*request_sequence,
*run_order,
);
}
OutputEvent::BashCommand { command } => {
self.finish_hard_stream_boundary();
let command = transcript::sanitize_preview(command);
self.push_transcript(transcript::TranscriptEntry::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::UsageSnapshot { .. } => {}
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.pending_assistant_completion_index = None;
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.active_assistant_transcript_index = self
.active_assistant_transcript_index
.or(self.pending_assistant_completion_index.take());
self.reconcile_assistant_text(text);
}
OutputEvent::Diagnostic { level, message } => {
self.finish_nonsemantic_stream_boundary();
self.push_transcript(transcript::TranscriptEntry::diagnostic(level, message));
}
OutputEvent::HookDiagnostic { diagnostic } => {
self.finish_nonsemantic_stream_boundary();
let message = diagnostic.sanitized_message();
self.push_transcript(transcript::TranscriptEntry::hook_diagnostic(&message));
}
OutputEvent::ProviderContextInjection { metadata } => {
self.finish_nonsemantic_stream_boundary();
self.push_transcript(transcript::TranscriptEntry::provider_context(
provider_context_injection_transcript_row(metadata),
));
}
OutputEvent::SubdirInstructionInjection { path, .. } => {
self.finish_nonsemantic_stream_boundary();
self.push_transcript(transcript::TranscriptEntry::subdir_instruction(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() {
let id = ActivityId::new(call.id.clone());
self.apply_activity_event(crate::output::ActivityEvent::ToolStartedDetail {
id,
detail: crate::tool_display::tool_activity_detail_pending(
call,
label,
crate::output::pending_activity_status(call),
),
});
self.start_tool_transcript(call, label);
}
}
OutputEvent::ToolResult {
call,
result,
summary,
changed_paths,
} => {
self.session_files.record(
&call.name,
result.success,
&result.metadata,
changed_paths,
);
self.finish_hard_stream_boundary();
self.finish_tool_transcript(
ActivityId::new(call.id.clone()),
call.name.clone(),
transcript_tool_row(call, summary),
summary.as_ref().clone(),
);
if !call.id.trim().is_empty() {
self.apply_activity_event(crate::output::ActivityEvent::ToolResultDetail {
id: ActivityId::new(call.id.clone()),
detail: crate::tool_display::tool_activity_detail(
call,
result,
summary.label.clone(),
if result.success {
ActivityStatus::Success
} else {
ActivityStatus::Failed
},
),
});
}
}
}
}
pub(crate) fn apply_compaction_history_summary_at(
&mut self,
summary: &str,
timestamp: Option<chrono::DateTime<chrono::Utc>>,
) {
let previous_timestamp = self.transcript_timestamp_context;
self.transcript_timestamp_context =
timestamp.map(|timestamp| timestamp.with_timezone(&chrono::Local));
self.record_compaction_complete_transcript(Some(summary));
self.transcript_timestamp_context = previous_timestamp;
}
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 original_index = self.transcript.len();
let timestamp = self.next_transcript_timestamp();
let drained = transcript::push_error_transcript(&mut self.transcript, message);
if self.transcript.len() != before || self.transcript.back() != last.as_ref() {
let _ = self.finish_transcript_append(original_index, drained, timestamp);
self.retain_valid_active_tool_transcripts();
self.invalidate_transcript_append_cache(drained);
}
}
pub(crate) fn record_fast_warning_transcript(&mut self, message: &str) {
let message = transcript::sanitize_preview(&crate::output::sanitize_display_text(message));
if message.trim().is_empty() {
return;
}
self.finish_hard_stream_boundary();
self.push_transcript(transcript::TranscriptEntry::warning(&message));
}
pub(crate) fn record_canceled_transcript(&mut self, prompt_preview: &str) {
self.finish_hard_stream_boundary();
let entry = transcript::TranscriptEntry::canceled(prompt_preview);
if self
.transcript
.back_entry()
.is_some_and(|last| last.rendered() == entry.rendered())
{
return;
}
self.push_transcript(entry);
}
pub(crate) fn record_compaction_started_transcript(&mut self) {
self.finish_hard_stream_boundary();
let entry = transcript::TranscriptEntry::compaction("started".to_string());
if let Some(absolute_index) = self.pending_compaction_transcript_index.take()
&& let Some(index) = self.transcript_local_index(absolute_index)
&& self.transcript.replace(index, entry.clone()).is_some()
{
self.invalidate_transcript_append_cache(0);
self.pending_compaction_transcript_index = Some(absolute_index);
return;
}
self.push_transcript(entry);
let local_index = self.transcript.len().saturating_sub(1);
self.pending_compaction_transcript_index = self.transcript_absolute_index(local_index);
}
pub(crate) fn record_compaction_complete_transcript(&mut self, summary: Option<&str>) {
self.finish_hard_stream_boundary();
let body = summary
.map(crate::output::redact_sensitive_text)
.map(|summary| {
Self::bounded_compaction_summary(&transcript::sanitize_preview(&summary))
})
.filter(|summary| !summary.trim().is_empty())
.map(|summary| format!("complete{PADDED_INLINE_SEPARATOR}{summary}"))
.unwrap_or_else(|| "complete".to_string());
let entry = transcript::TranscriptEntry::compaction(body);
if let Some(absolute_index) = self.pending_compaction_transcript_index.take()
&& let Some(index) = self.transcript_local_index(absolute_index)
&& self.transcript.replace(index, entry.clone()).is_some()
{
self.invalidate_transcript_append_cache(0);
return;
}
self.push_transcript(entry);
}
pub(crate) fn finish_compaction_failure_transcript(&mut self, message: &str, canceled: bool) {
self.finish_hard_stream_boundary();
let entry = if canceled {
transcript::TranscriptEntry::canceled(message)
} else {
transcript::TranscriptEntry::error(message)
};
if let Some(absolute_index) = self.pending_compaction_transcript_index.take()
&& let Some(index) = self.transcript_local_index(absolute_index)
&& self.transcript.replace(index, entry.clone()).is_some()
{
self.invalidate_transcript_append_cache(0);
return;
}
self.push_transcript(entry);
}
const COMPACTION_TRANSCRIPT_MAX_BYTES: usize = 4 * 1024;
const COMPACTION_TRANSCRIPT_MAX_LINES: usize = 32;
const COMPACTION_TRANSCRIPT_TRUNCATION_MARKER: &str = "… [truncated]";
fn bounded_compaction_summary(summary: &str) -> String {
let mut output = String::new();
let mut truncated = false;
for (index, line) in summary
.lines()
.take(Self::COMPACTION_TRANSCRIPT_MAX_LINES)
.enumerate()
{
let separator = usize::from(index > 0);
if output.len() + separator + line.len() > Self::COMPACTION_TRANSCRIPT_MAX_BYTES {
truncated = true;
break;
}
if index > 0 {
output.push('\n');
}
output.push_str(line);
}
if summary.lines().count() > Self::COMPACTION_TRANSCRIPT_MAX_LINES
|| output.len() < summary.len()
{
truncated = true;
}
if truncated {
while output.len() + 1 + Self::COMPACTION_TRANSCRIPT_TRUNCATION_MARKER.len()
> Self::COMPACTION_TRANSCRIPT_MAX_BYTES
{
output.pop();
while !output.is_char_boundary(output.len()) {
output.pop();
}
}
if !output.is_empty() {
output.push('\n');
}
output.push_str(Self::COMPACTION_TRANSCRIPT_TRUNCATION_MARKER);
}
output
}
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));
}
fn next_transcript_timestamp(&self) -> chrono::DateTime<chrono::Local> {
self.transcript_timestamp_context
.unwrap_or_else(chrono::Local::now)
}
fn finish_transcript_append(
&mut self,
original_index: usize,
drained: usize,
timestamp: chrono::DateTime<chrono::Local>,
) -> Option<usize> {
self.normalize_transcript_indices_after_drain(drained);
let local_index = original_index
.checked_sub(drained)
.filter(|index| *index < self.transcript.len());
if let Some(local_index) = local_index
&& let Some(absolute_index) = self.transcript_absolute_index(local_index)
{
self.transcript.set_timestamp(absolute_index, timestamp);
}
local_index
}
fn append_transcript_entry(
&mut self,
entry: transcript::TranscriptEntry,
) -> (usize, Option<usize>) {
let original_index = self.transcript.len();
let timestamp = self.next_transcript_timestamp();
let drained = transcript::push_transcript(&mut self.transcript, entry);
let local_index = self.finish_transcript_append(original_index, drained, timestamp);
self.retain_valid_active_tool_transcripts();
self.invalidate_transcript_append_cache(drained);
(drained, local_index)
}
pub(super) fn push_transcript<T>(&mut self, entry: T)
where
T: Into<transcript::TranscriptEntry>,
{
let _ = self.append_transcript_entry(entry.into());
}
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(),
tool_name: call.name.clone(),
label,
activity,
subagents,
};
let (_, index) = self.append_transcript_entry(active.render_entry());
let index = index.unwrap_or(self.transcript.len().saturating_sub(1));
let absolute_index = self
.transcript_absolute_index(index)
.unwrap_or(self.transcript.index_base());
let mut active = active;
active.index = absolute_index;
self.transcript.link_activity(
absolute_index,
TranscriptActivityLink {
activity_id: id.clone(),
tool_name: call.name.clone(),
},
);
self.active_tool_transcripts.insert(id, active);
}
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 entry = active.render_entry();
self.active_tool_transcripts.insert(id.clone(), active);
if let Some(transcript_entry) = self.transcript.get_mut(index) {
*transcript_entry = entry;
self.invalidate_transcript_entry_cache(index);
}
}
fn finish_tool_transcript(
&mut self,
id: ActivityId,
tool_name: String,
final_row: String,
summary: crate::output::ToolDisplaySummary,
) {
let entry = transcript::TranscriptEntry::tool(
normalize_inline_separators(&transcript::sanitize_preview(&final_row)),
transcript::TranscriptToolData {
identity: transcript::TranscriptToolIdentity::Summary(summary),
children: self
.active_tool_transcripts
.get(&id)
.and_then(|active| self.transcript_local_index(active.index))
.and_then(|index| self.transcript.get(index))
.and_then(transcript::TranscriptEntry::tool_data)
.map(|data| data.children.clone())
.unwrap_or_default(),
},
);
if let Some(active) = self.active_tool_transcripts.remove(&id)
&& let Some(index) = self.transcript_local_index(active.index)
&& let Some(transcript_entry) = self.transcript.get_mut(index)
{
*transcript_entry = entry;
self.invalidate_transcript_entry_cache(index);
return;
}
let (_, normalized_index) = self.append_transcript_entry(entry);
if !id.as_str().trim().is_empty() {
let normalized_index =
normalized_index.unwrap_or(self.transcript.len().saturating_sub(1));
let absolute_index = self
.transcript_absolute_index(normalized_index)
.unwrap_or(self.transcript.index_base());
self.transcript.link_activity(
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));
let valid_subagent_card_batches = self
.transcript
.activity_links()
.values()
.filter(|link| link.tool_name == "subagents")
.map(|link| link.activity_id.clone())
.collect::<std::collections::HashSet<_>>();
self.subagent_card_selection
.retain(|batch_id, _| valid_subagent_card_batches.contains(batch_id));
}
fn normalize_transcript_indices_after_drain(&mut self, drained: usize) {
if drained == 0 {
return;
}
let old_base = self.transcript.index_base().saturating_sub(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.pending_assistant_completion_index = self
.pending_assistant_completion_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_nonsemantic_stream_boundary(&mut self) {
self.finish_thinking_live_redaction();
self.active_thinking_transcript_index = None;
self.thinking_transcript_identities.clear();
}
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.pending_assistant_completion_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;
}
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(|entry| entry.kind().is_assistant())
{
return;
}
self.active_assistant_transcript_index = None;
let (_, index) = self.append_transcript_entry(transcript::TranscriptEntry::assistant(""));
self.active_assistant_transcript_index = index;
}
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(entry) = self.transcript.get_mut(index)
&& entry.kind().is_assistant()
{
entry.append_assistant_text(&visible_delta);
self.invalidate_transcript_entry_cache(index);
return;
}
self.active_assistant_transcript_index = None;
let (_, index) =
self.append_transcript_entry(transcript::TranscriptEntry::assistant(&visible_delta));
self.active_assistant_transcript_index = index;
}
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 original_index = self.transcript.len();
let timestamp = self.next_transcript_timestamp();
let (drained, index) = transcript::reconcile_assistant_text(
&mut self.transcript,
text,
self.active_assistant_transcript_index,
);
self.normalize_transcript_indices_after_drain(drained);
if index == original_index.checked_sub(drained)
&& let Some(local_index) = index
&& let Some(absolute_index) = self.transcript_absolute_index(local_index)
{
self.transcript.set_timestamp(absolute_index, timestamp);
}
self.active_assistant_transcript_index = None;
self.pending_assistant_completion_index = index;
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(entry) = self.transcript.get_mut(index)
&& entry.kind().is_thinking()
{
entry.append_thinking_text(&visible_delta);
self.invalidate_transcript_entry_cache(index);
return;
}
let (_, index) =
self.append_transcript_entry(transcript::TranscriptEntry::thinking(&visible_delta));
self.active_thinking_transcript_index = index;
}
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.trim().is_empty() {
self.active_thinking_transcript_index = None;
return;
}
let final_entry = transcript::TranscriptEntry::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(identity_index) = self
.thinking_transcript_identities
.get(identity_key)
.copied()
&& let Some(active_index) = self.active_thinking_transcript_index
&& active_index != identity_index
&& active_index + 1 == self.transcript.len()
&& (identity_index..=active_index).all(|index| {
self.transcript
.get(index)
.is_some_and(|entry| entry.kind().is_thinking())
})
&& !self
.thinking_transcript_identities
.iter()
.any(|(key, index)| key != identity_key && *index == active_index)
{
let active_absolute_index = self.transcript_absolute_index(active_index);
self.transcript.pop_back();
self.active_thinking_transcript_index = None;
self.retain_valid_active_tool_transcripts();
if let Some(active_absolute_index) = active_absolute_index {
self.transcript_cache
.borrow_mut()
.evict_card(active_absolute_index);
}
self.invalidate_transcript_visual_projection_cache();
}
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(|entry| entry.kind().is_thinking())
{
if let Some(entry) = self.transcript.get_mut(index) {
*entry = final_entry.clone();
}
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(entry) = self.transcript.get_mut(index)
&& entry.kind().is_thinking()
{
*entry = final_entry.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_thinking_entry(&self.transcript)
&& self.transcript.get(index).is_some_and(|entry| {
!active_thinking_text.is_empty()
&& transcript::body_matches(entry.body(), &active_thinking_text)
})
{
if let Some(entry) = self.transcript.get_mut(index) {
*entry = final_entry.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 (_, local_index) = self.append_transcript_entry(final_entry);
if let Some(identity_key) = identity_key
&& let Some(local_index) = local_index
{
self.thinking_transcript_identities
.insert(identity_key, local_index);
}
self.active_thinking_transcript_index = None;
}
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(),
transcript_tool_name: None,
transcript_subagent: None,
transcript_reasoning: None,
transcript_bash_command: None,
parent_id: None,
parent_provenance: crate::tui::activity::ActivityParentProvenance::Unavailable,
kind: ActivityKind::Diagnostic,
status,
metadata: metadata.clone(),
started_at: chrono::Local::now(),
update_sequence: 0,
own_update_sequence: 0,
preview: String::new(),
preview_truncated: false,
usage: None,
last_known_cache_percent: None,
last_known_latest_cache_percent: None,
tool_detail: None,
hash_edit_tree_summary: None,
},
);
}
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);
}
}
}
impl MissionControlState {
fn reset_context_for_new_prompt(&mut self) {
if let Some(usage) = &mut self.context_usage {
usage.reasoning_tokens = None;
usage.source = ContextUsageSource::FallbackEstimate;
usage.request_sequence = 0;
}
}
}