mod context_injection;
mod hooks;
mod subagent_history;
use context_injection::provider_context_content_pairs;
use context_injection::hydrate_hook_context_injection_event;
use context_injection::hydrate_subdir_instruction_load_event;
use subagent_history::hydrate_subagent_tool_call;
use subagent_history::hydrate_subagent_tool_result;
use subagent_history::finish_outstanding_subagent_tasks;
use hooks::hydrate_diagnostic_event;
use hooks::hydrate_hook_diagnostic_event;
use hooks::hydrate_hook_lifecycle_event;
use hooks::parse_hook_phase;
use crate::{
config::{HookFailurePolicy, InjectedContentSettings, InjectedContentStyle},
hooks::{HookDiagnostic, HookFailureCategory, HookPhase},
output::{
ActivityEvent, ActivityId, ActivityKind, ActivityMetadata, ActivityStatus, OutputEvent,
ProviderContextInjectionDisplay, redact_sensitive_text,
},
providers::ToolCall,
sessions::{SessionEvent, SessionEventKind, TurnStatus},
subagents::{
MAX_SUBAGENT_TASKS, SubagentStatus, SubagentTask, SubagentTaskResult, SubagentsArgs,
SubagentsOutput,
},
tools::{ToolResult, contract::tool_name},
tui::{activity::ReasoningSummaryGroup, state},
};
pub(crate) fn hydrate_session_history(
ui_state: &mut state::MissionControlState,
events: &[SessionEvent],
injected_content_settings: &InjectedContentSettings,
) {
ui_state.session_active_time =
crate::sessions::active_time::SessionActiveTime::from_events(events);
if let Some(event) = events.first() {
ui_state.session_files.session_id = Some(event.session_id.clone());
ui_state.session_files.cwd = event.cwd.clone();
}
let assessments = crate::protection::prompt_injection::RecordedAssessments::new(events);
let mut primary_usage_since_output = false;
let mut recorded_primary_outputs = std::collections::HashSet::new();
for (index, event) in events.iter().enumerate() {
if event.kind() == Some(SessionEventKind::Compaction)
&& let Some(value) = event.payload.get("session_usage")
{
match crate::sessions::usage::SessionUsageLedger::from_checkpoint(value.clone()) {
Ok(ledger) => ui_state.session_usage = ledger,
Err(_) => ui_state.session_usage.incomplete = true,
}
ui_state.refresh_session_cache_percent();
primary_usage_since_output = event
.payload
.get("primary_usage_since_output")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
}
if event.kind() == Some(SessionEventKind::Compaction)
&& event.payload.get("aggregate").is_some()
&& event.payload.get("session_usage").is_none()
{
ui_state.session_usage.incomplete = true;
}
if event.kind() == Some(SessionEventKind::AssistantOutput) {
if primary_usage_since_output {
recorded_primary_outputs.insert(index);
}
primary_usage_since_output = false;
}
if event.turn_status_payload().is_some_and(|status| {
matches!(status.status, TurnStatus::Cancelled | TurnStatus::Failed)
}) {
if !primary_usage_since_output {
ui_state.session_usage.incomplete = true;
}
primary_usage_since_output = false;
}
if event.kind() == Some(SessionEventKind::SessionUsage) {
match serde_json::from_value::<crate::tui::session_usage::SessionUsageRecord>(
event.payload.clone(),
) {
Ok(record) => {
if record.id.contains("/primary/") {
primary_usage_since_output = true;
}
ui_state.session_usage.observe(record);
}
Err(_) => ui_state.session_usage.incomplete = true,
}
ui_state.refresh_session_cache_percent();
}
}
let provider_context_content =
provider_context_content_pairs(events, injected_content_settings);
let mut tool_calls = std::collections::HashMap::<String, ToolCall>::new();
let mut tool_activity_ids = std::collections::HashMap::<String, ActivityId>::new();
let mut hook_activity_sequence = 0usize;
let mut started_hooks = std::collections::HashSet::<ActivityId>::new();
let mut pending_assistant_text = String::new();
let mut pending_assistant_timestamp = None;
let mut projected_assistant_chunk_text = String::new();
let mut assistant_chunks_in_current_response = false;
let mut restored_terminal_assistant_text = false;
let mut prior_assistant_chunk_text = String::new();
let mut reasoning_group = ReasoningSummaryGroup::default();
let mut reasoning_turn_sequence = 0usize;
for (event_index, event) in events.iter().enumerate() {
match event.kind() {
Some(SessionEventKind::SessionUsage) => {}
Some(SessionEventKind::Compaction) => {
flush_pending_assistant_history(
ui_state,
&mut pending_assistant_text,
&mut projected_assistant_chunk_text,
&mut pending_assistant_timestamp,
);
if let Some(summary) = event
.payload
.get("summary")
.and_then(serde_json::Value::as_str)
{
ui_state.apply_compaction_history_summary_at(summary, Some(event.timestamp));
}
}
Some(SessionEventKind::UserInput) => {
flush_pending_assistant_history(
ui_state,
&mut pending_assistant_text,
&mut projected_assistant_chunk_text,
&mut pending_assistant_timestamp,
);
projected_assistant_chunk_text.clear();
prior_assistant_chunk_text.clear();
reasoning_group.reset();
reasoning_turn_sequence = reasoning_turn_sequence.saturating_add(1);
assistant_chunks_in_current_response = false;
restored_terminal_assistant_text = false;
if let Some(text) = event
.payload
.get("text")
.and_then(serde_json::Value::as_str)
{
let output_event = if event
.payload
.get("origin")
.and_then(serde_json::Value::as_str)
== Some("automatic_compaction")
{
OutputEvent::AutomaticUserPrompt {
text: text.to_string(),
}
} else {
OutputEvent::UserPrompt {
text: text.to_string(),
}
};
ui_state.apply_output_event_at(&output_event, Some(event.timestamp));
}
}
Some(SessionEventKind::AssistantChunk) => {
if let Some(text) = event
.payload
.get("text")
.and_then(serde_json::Value::as_str)
{
if !text.trim().is_empty() {
assistant_chunks_in_current_response = true;
}
if !text.is_empty() {
pending_assistant_timestamp.get_or_insert(event.timestamp);
}
pending_assistant_text.push_str(text);
}
}
Some(SessionEventKind::AssistantOutput) => {
if !recorded_primary_outputs.contains(&event_index) {
let raw = event.payload.get("usage");
let tokens = |key| {
raw.and_then(|value| value.get(key))
.and_then(serde_json::Value::as_u64)
.unwrap_or_default()
};
ui_state.session_usage.incomplete = true;
ui_state
.session_usage
.observe(crate::tui::session_usage::SessionUsageRecord {
id: format!("legacy/primary/{event_index}"),
usage: crate::output::NormalizedUsageSnapshot {
effective_input: tokens("input"),
output: tokens("output"),
cache_read: tokens("cache_read"),
cache_known: false,
},
request_sequence: 0,
final_usage: true,
});
}
let output_text = event
.payload
.get("text")
.and_then(serde_json::Value::as_str)
.unwrap_or(&pending_assistant_text)
.to_string();
pending_assistant_text.clear();
let assistant_timestamp = pending_assistant_timestamp
.take()
.unwrap_or(event.timestamp);
let text = crate::sessions::chat::reconciled_assistant_output(
&output_text,
&prior_assistant_chunk_text,
&projected_assistant_chunk_text,
);
projected_assistant_chunk_text.clear();
prior_assistant_chunk_text.clear();
if !text.is_empty() {
ui_state.apply_output_event_at(
&OutputEvent::AssistantComplete { text },
Some(assistant_timestamp),
);
}
}
Some(SessionEventKind::ReasoningSummary) => {
flush_pending_assistant_history(
ui_state,
&mut pending_assistant_text,
&mut projected_assistant_chunk_text,
&mut pending_assistant_timestamp,
);
if let Some(text) = event
.payload
.get("text")
.and_then(serde_json::Value::as_str)
{
let persisted_item_id = event
.payload
.get("item_id")
.and_then(serde_json::Value::as_str);
if let Some(item_id) = persisted_item_id {
let turn_id = event
.payload
.get("turn_id")
.and_then(serde_json::Value::as_str)
.map(str::to_string)
.unwrap_or_else(|| reasoning_turn_sequence.to_string());
let activity_id =
ActivityId::new(format!("history/reasoning/{turn_id}/{item_id}"));
if let Some(activity_event) =
reasoning_group.append_identity_event(activity_id, None, text)
{
ui_state.apply_historical_activity_event(activity_event);
}
ui_state.apply_output_event_at(
&OutputEvent::ThinkingSummaryCompleteIdentified {
text: text.to_string(),
item_id: Some(item_id.to_string()),
turn_id: Some(turn_id),
},
Some(event.timestamp),
);
} else {
let item_id = format!("legacy-{reasoning_turn_sequence}-{event_index}");
let turn_id = reasoning_turn_sequence.to_string();
if let Some(activity_event) = reasoning_group.append_identity_event(
ActivityId::new(format!("history/reasoning/{turn_id}/{item_id}")),
None,
text,
) {
ui_state.apply_historical_activity_event(activity_event);
}
ui_state.apply_output_event_at(
&OutputEvent::ThinkingSummaryCompleteIdentified {
text: text.to_string(),
item_id: Some(item_id),
turn_id: Some(turn_id),
},
Some(event.timestamp),
);
}
}
}
Some(SessionEventKind::ToolCall | SessionEventKind::CodeModeToolCall) => {
flush_pending_assistant_history(
ui_state,
&mut pending_assistant_text,
&mut projected_assistant_chunk_text,
&mut pending_assistant_timestamp,
);
prior_assistant_chunk_text.push_str(&projected_assistant_chunk_text);
projected_assistant_chunk_text.clear();
let Some(call) = session_event_tool_call(event) else {
continue;
};
let activity_id = ActivityId::new(history_tool_activity_id(&call));
let nested = event.kind() == Some(SessionEventKind::CodeModeToolCall);
let parent_id = event.payload["parent_call_id"]
.as_str()
.and_then(|parent| tool_activity_ids.get(parent))
.cloned();
if !call.id.trim().is_empty() {
tool_activity_ids.insert(call.id.clone(), activity_id.clone());
tool_calls.insert(call.id.clone(), call.clone());
}
ui_state.apply_historical_activity_event(ActivityEvent::Started {
id: activity_id.clone(),
parent_id,
kind: ActivityKind::Tool,
status: crate::output::pending_activity_status(&call),
metadata: ActivityMetadata::new(crate::output::tool_display_label(&call)),
});
let mut display_call = call.clone();
display_call.id = activity_id.as_str().to_string();
if nested {
ui_state.apply_historical_activity_event(ActivityEvent::ToolStartedDetail {
id: activity_id.clone(),
detail: crate::output::tool_display::tool_activity_detail_pending(
&display_call,
crate::output::tool_display_label(&call),
crate::output::pending_activity_status(&call),
),
});
hydrate_subagent_tool_call(&mut *ui_state, &activity_id, &call);
continue;
}
ui_state.apply_output_event_at(
&OutputEvent::ToolStarted {
label: crate::output::tool_display_label(&call),
call: Box::new(display_call),
},
Some(event.timestamp),
);
hydrate_subagent_tool_call(&mut *ui_state, &activity_id, &call);
}
Some(
SessionEventKind::ToolResult
| SessionEventKind::ToolDisplayResult
| SessionEventKind::CodeModeToolResult,
) => {
flush_pending_assistant_history(
ui_state,
&mut pending_assistant_text,
&mut projected_assistant_chunk_text,
&mut pending_assistant_timestamp,
);
prior_assistant_chunk_text.push_str(&projected_assistant_chunk_text);
projected_assistant_chunk_text.clear();
match session_event_tool_result(event, &tool_calls) {
Ok((call, mut result)) => {
let assessment = assessments.for_result(event_index);
if !assessment.is_null() {
result.metadata["prompt_injection_protection"] = assessment.clone();
}
tool_calls.remove(&call.id);
let activity_id = tool_activity_ids
.get(&call.id)
.cloned()
.unwrap_or_else(|| ActivityId::new(history_tool_activity_id(&call)));
if call.name == tool_name::SUBAGENTS {
hydrate_subagent_tool_result(
&mut *ui_state,
&activity_id,
&call,
&result,
);
}
let mut display_call = call.clone();
display_call.id = activity_id.as_str().to_string();
if event.kind() == Some(SessionEventKind::CodeModeToolResult) {
let status = if result.success {
ActivityStatus::Success
} else {
ActivityStatus::Failed
};
ui_state.apply_historical_activity_event(
ActivityEvent::ToolResultDetail {
id: activity_id.clone(),
detail: crate::output::tool_display::tool_activity_detail(
&display_call,
&result,
crate::output::tool_display_label(&call),
status,
),
},
);
ui_state.apply_historical_activity_event(ActivityEvent::Finished {
id: activity_id,
status,
metadata: None,
});
continue;
}
ui_state.apply_output_event_at(
&OutputEvent::ToolResult {
changed_paths: Vec::new(),
summary: Box::new(crate::output::tool_display_summary(
&call, &result,
)),
call: Box::new(display_call),
result: Box::new(result),
},
Some(event.timestamp),
);
}
Err(message) => {
ui_state.apply_output_event_at(
&OutputEvent::Diagnostic {
level: "warning".to_string(),
message,
},
Some(event.timestamp),
);
}
}
}
Some(SessionEventKind::TurnStatus) => {
let Some(payload) = event.turn_status_payload() else {
continue;
};
if matches!(
payload.status,
TurnStatus::Cancelled | TurnStatus::Failed | TurnStatus::CompactionRequired
) && !assistant_chunks_in_current_response
&& pending_assistant_text.trim().is_empty()
&& !restored_terminal_assistant_text
&& let Some(text) = payload.assistant_text.as_deref()
&& !text.trim().is_empty()
{
ui_state.apply_output_event_at(
&OutputEvent::AssistantComplete {
text: text.to_string(),
},
Some(event.timestamp),
);
restored_terminal_assistant_text = true;
}
let status = match payload.status {
TurnStatus::Cancelled => Some(ActivityStatus::Canceled),
TurnStatus::Failed => Some(ActivityStatus::Failed),
_ => None,
};
if let Some(status) = status {
for (call_id, call) in &tool_calls {
if let Some(id) = tool_activity_ids.get(call_id) {
if call.name == tool_name::SUBAGENTS {
finish_outstanding_subagent_tasks(&mut *ui_state, id, status);
}
ui_state.apply_historical_activity_event(ActivityEvent::Finished {
id: id.clone(),
status,
metadata: None,
});
}
}
tool_calls.clear();
}
}
Some(SessionEventKind::HookLifecycle) => {
flush_pending_assistant_history(
ui_state,
&mut pending_assistant_text,
&mut projected_assistant_chunk_text,
&mut pending_assistant_timestamp,
);
hydrate_hook_lifecycle_event(
ui_state,
event,
&tool_activity_ids,
&mut started_hooks,
&mut hook_activity_sequence,
);
}
Some(SessionEventKind::HookContextInjection) => {
flush_pending_assistant_history(
ui_state,
&mut pending_assistant_text,
&mut projected_assistant_chunk_text,
&mut pending_assistant_timestamp,
);
hydrate_hook_context_injection_event(
ui_state,
event,
injected_content_settings,
&provider_context_content,
&tool_activity_ids,
);
}
Some(SessionEventKind::Diagnostic) => {
flush_pending_assistant_history(
ui_state,
&mut pending_assistant_text,
&mut projected_assistant_chunk_text,
&mut pending_assistant_timestamp,
);
hydrate_diagnostic_event(ui_state, event);
}
Some(SessionEventKind::HookDiagnostic) => {
flush_pending_assistant_history(
ui_state,
&mut pending_assistant_text,
&mut projected_assistant_chunk_text,
&mut pending_assistant_timestamp,
);
hydrate_hook_diagnostic_event(ui_state, event);
}
Some(SessionEventKind::SubdirInstructionLoad) => {
flush_pending_assistant_history(
ui_state,
&mut pending_assistant_text,
&mut projected_assistant_chunk_text,
&mut pending_assistant_timestamp,
);
hydrate_subdir_instruction_load_event(ui_state, event, &tool_activity_ids);
}
_ => {}
}
}
flush_pending_assistant_history(
ui_state,
&mut pending_assistant_text,
&mut projected_assistant_chunk_text,
&mut pending_assistant_timestamp,
);
ui_state.finish_assistant_streaming();
ui_state.session_usage.fold_completed_run();
}
fn flush_pending_assistant_history(
ui_state: &mut state::MissionControlState,
pending_assistant_text: &mut String,
projected_assistant_chunk_text: &mut String,
pending_assistant_timestamp: &mut Option<chrono::DateTime<chrono::Utc>>,
) {
if !pending_assistant_text.is_empty() {
let text = std::mem::take(pending_assistant_text);
projected_assistant_chunk_text.push_str(&text);
let timestamp = pending_assistant_timestamp.take();
ui_state.apply_output_event_at(&OutputEvent::AssistantDelta { text }, timestamp);
} else {
pending_assistant_timestamp.take();
}
}
use crate::sessions::chat::{
tool_call as session_event_tool_call, tool_result as session_event_tool_result,
};
fn sanitized_payload_str(payload: &serde_json::Value, key: &str) -> Option<String> {
payload
.get(key)
.and_then(serde_json::Value::as_str)
.map(redact_sensitive_text)
}
fn history_tool_activity_id(call: &ToolCall) -> String {
if call.id.trim().is_empty() {
format!("history/tool-{}", call.name)
} else {
call.id.clone()
}
}