use super::*;
pub(super) fn provider_context_content_pairs(
events: &[SessionEvent],
settings: &InjectedContentSettings,
) -> std::collections::HashMap<String, String> {
if settings.style != InjectedContentStyle::Content {
return std::collections::HashMap::new();
}
let injections = events
.iter()
.filter(|event| event.kind() == Some(SessionEventKind::HookContextInjection))
.filter_map(HydratedContextInjection::from_event)
.collect::<Vec<_>>();
let provider_items = events
.iter()
.filter(|event| event.kind() == Some(SessionEventKind::ProviderContextItem))
.filter(|event| {
event
.payload
.get("source")
.and_then(serde_json::Value::as_str)
!= Some("subdir_agents")
})
.filter_map(provider_context_item_content)
.collect::<Vec<_>>();
let successful = injections
.iter()
.filter(|injection| injection.status == "success" && injection.item_count > 0)
.collect::<Vec<_>>();
let total_items = successful
.iter()
.map(|injection| injection.item_count)
.sum::<usize>();
if successful.is_empty() || total_items != provider_items.len() {
return std::collections::HashMap::new();
}
let can_pair_by_order = successful
.iter()
.all(|injection| injection.injection_id.is_some())
|| successful.len() == 1;
if !can_pair_by_order {
return std::collections::HashMap::new();
}
let mut pairs = std::collections::HashMap::new();
let mut cursor = 0usize;
for injection in successful {
let end = cursor.saturating_add(injection.item_count);
let content = provider_items[cursor..end].join("\n");
if !content.is_empty() {
pairs.insert(injection.content_key(), bounded_injected_content(&content));
}
cursor = end;
}
pairs
}
#[derive(Debug, Clone)]
pub(super) struct HydratedContextInjection {
pub(super) phase: HookPhase,
pub(super) label: String,
target: String,
pub(super) status: String,
item_count: usize,
byte_count: usize,
hook_index: usize,
pub(super) tool_call_id: Option<String>,
injection_id: Option<String>,
}
impl HydratedContextInjection {
fn from_event(event: &SessionEvent) -> Option<Self> {
let payload = &event.payload;
Some(Self {
phase: parse_hook_phase(payload.get("phase")?.as_str()?),
label: sanitized_payload_str(payload, "label").unwrap_or_else(|| "hook".to_string()),
target: sanitized_payload_str(payload, "target_tool")
.or_else(|| sanitized_payload_str(payload, "target"))
.unwrap_or_else(|| "unknown".to_string()),
status: sanitized_payload_str(payload, "status")
.unwrap_or_else(|| "unknown".to_string()),
item_count: payload
.get("item_count")
.and_then(serde_json::Value::as_u64)
.unwrap_or(0) as usize,
byte_count: payload
.get("byte_count")
.and_then(serde_json::Value::as_u64)
.unwrap_or(0) as usize,
hook_index: payload
.get("hook_index")
.and_then(serde_json::Value::as_u64)
.unwrap_or(0) as usize,
tool_call_id: sanitized_payload_str(payload, "tool_call_id"),
injection_id: sanitized_payload_str(payload, "injection_id"),
})
}
fn content_key(&self) -> String {
self.injection_id.clone().unwrap_or_else(|| {
format!(
"{}:{}:{}",
self.phase.as_str(),
self.target,
self.hook_index
)
})
}
}
fn provider_context_item_content(event: &SessionEvent) -> Option<String> {
let role = event.payload.get("role")?.as_str()?;
if role != "user" {
return None;
}
event
.payload
.get("content")
.and_then(serde_json::Value::as_str)
.map(str::to_string)
}
pub(super) fn hydrate_hook_context_injection_event(
ui_state: &mut state::MissionControlState,
event: &SessionEvent,
settings: &InjectedContentSettings,
content_pairs: &std::collections::HashMap<String, String>,
tool_activity_ids: &std::collections::HashMap<String, ActivityId>,
) {
if !settings.show_in_transcript && !settings.show_in_activity_tree {
return;
}
let Some(injection) = HydratedContextInjection::from_event(event) else {
return;
};
let content =
if settings.style == InjectedContentStyle::Content && injection.status == "success" {
injection
.injection_id
.as_ref()
.and_then(|id| content_pairs.get(id).cloned())
.or_else(|| content_pairs.get(&injection.content_key()).cloned())
} else {
None
};
let display = ProviderContextInjectionDisplay::new(
injection.phase,
injection.label.clone(),
injection.target.clone(),
injection.status.clone(),
injection.item_count,
injection.byte_count,
content,
);
if settings.show_in_transcript {
ui_state.apply_output_event_at(
&OutputEvent::ProviderContextInjection {
metadata: display.clone(),
},
Some(event.timestamp),
);
}
if settings.show_in_activity_tree {
hydrate_provider_context_injection_activity(
ui_state,
&injection,
&display,
tool_activity_ids,
);
}
}
fn hydrate_provider_context_injection_activity(
ui_state: &mut state::MissionControlState,
injection: &HydratedContextInjection,
display: &ProviderContextInjectionDisplay,
tool_activity_ids: &std::collections::HashMap<String, ActivityId>,
) {
let parent_id = injection
.tool_call_id
.as_deref()
.and_then(|call_id| tool_activity_ids.get(call_id).cloned());
let id = provider_context_injection_activity_id(parent_id.as_ref(), injection);
let metadata = provider_context_injection_activity_metadata(display);
let status = if injection.status == "success" {
ActivityStatus::Success
} else {
ActivityStatus::Failed
};
ui_state.apply_historical_activity_event(ActivityEvent::Started {
id: id.clone(),
parent_id,
kind: ActivityKind::ProviderContextInjection,
status: ActivityStatus::Running,
metadata: metadata.clone(),
});
ui_state.apply_historical_activity_event(ActivityEvent::Finished {
id,
status,
metadata: Some(metadata),
});
}
fn provider_context_injection_activity_id(
parent_activity_id: Option<&ActivityId>,
injection: &HydratedContextInjection,
) -> ActivityId {
let phase = injection.phase.as_str();
if let Some(parent_activity_id) = parent_activity_id {
parent_activity_id.child(format!(
"{phase}-provider-context-injection-{}",
injection.content_key()
))
} else {
ActivityId::new(format!(
"provider-context-injection/{}/{}/{}",
injection.target,
phase,
injection.content_key()
))
}
}
fn provider_context_injection_activity_metadata(
display: &ProviderContextInjectionDisplay,
) -> ActivityMetadata {
ActivityMetadata {
label: format!("provider context injection {}", display.label),
detail: Some(format!(
"provider context injection '{}' for {} {}: {} {} {} bytes",
display.label,
display.target,
display.phase.as_str(),
display.status,
item_count_text(display.item_count),
display.byte_count
)),
fields: vec![
("phase".to_string(), display.phase.as_str().to_string()),
("hook_label".to_string(), display.label.clone()),
("target".to_string(), display.target.clone()),
("status".to_string(), display.status.clone()),
("item_count".to_string(), display.item_count.to_string()),
("bytes".to_string(), display.byte_count.to_string()),
],
}
}
fn item_count_text(count: usize) -> String {
if count == 1 {
"1 item".to_string()
} else {
format!("{count} items")
}
}
fn bounded_injected_content(text: &str) -> String {
const MAX_INJECTED_CONTENT_CHARS: usize = 2000;
let redacted = redact_sensitive_text(text);
let mut chars = redacted.chars();
let truncated = chars
.by_ref()
.take(MAX_INJECTED_CONTENT_CHARS)
.collect::<String>();
if chars.next().is_some() {
format!("{truncated}…")
} else {
truncated
}
}
pub(super) fn hydrate_subdir_instruction_load_event(
ui_state: &mut state::MissionControlState,
event: &SessionEvent,
tool_activity_ids: &std::collections::HashMap<String, ActivityId>,
) {
let payload = &event.payload;
if payload.get("source").and_then(serde_json::Value::as_str) != Some("subdir_agents") {
return;
}
if payload.get("status").and_then(serde_json::Value::as_str) != Some("success") {
return;
}
let Some(path) = sanitized_payload_str(payload, "path") else {
return;
};
let bytes = payload
.get("bytes")
.and_then(serde_json::Value::as_u64)
.unwrap_or(0) as usize;
let parent_activity_id = payload
.get("tool_call_id")
.and_then(serde_json::Value::as_str)
.and_then(|call_id| tool_activity_ids.get(call_id))
.map(|id| id.as_str().to_string());
ui_state.apply_output_event_at(
&OutputEvent::SubdirInstructionInjection {
path: std::path::PathBuf::from(&path),
bytes,
parent_activity_id: parent_activity_id.clone(),
},
Some(event.timestamp),
);
let id = ActivityId::new(format!(
"history/subdir-instruction-load/{}",
event.timestamp.timestamp_micros()
));
let metadata = ActivityMetadata {
label: format!("Loaded subdirectory instructions: {path}"),
detail: Some(format!(
"Loaded subdirectory instructions: {path} ({bytes} bytes)"
)),
fields: vec![
("source".to_string(), "subdir_agents".to_string()),
("path".to_string(), path),
("status".to_string(), "success".to_string()),
("bytes".to_string(), bytes.to_string()),
],
};
ui_state.apply_historical_activity_event(ActivityEvent::Started {
id: id.clone(),
parent_id: parent_activity_id.map(ActivityId::new),
kind: ActivityKind::ProviderContextInjection,
status: ActivityStatus::Running,
metadata: metadata.clone(),
});
ui_state.apply_historical_activity_event(ActivityEvent::Finished {
id,
status: ActivityStatus::Success,
metadata: Some(metadata),
});
}