mod parent_graph;
mod subagent_transcript;
use super::transcript_state::{ContextUsageState, merge_context_usage, transcript_metadata_fields};
use crate::output::{
ActivityEvent, ActivityId, ActivityKind, ActivityMetadata, ActivityStatus, ContextUsageSource,
};
use crate::tui::{PADDED_INLINE_SEPARATOR, normalize_inline_separators, transcript};
use std::collections::VecDeque;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ActiveToolTranscript {
pub(super) index: usize,
pub(super) tool_name: String,
pub(super) label: String,
pub(super) activity: String,
pub(super) subagents: Option<ActiveSubagentTranscript>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct ActiveSubagentTranscript {
pub(super) tasks: Vec<ActiveSubagentTask>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct ActiveSubagentTask {
pub(super) id: ActivityId,
pub(super) label: String,
pub(super) status: ActivityStatus,
usage: Option<ContextUsageState>,
recent: VecDeque<String>,
}
impl ActiveToolTranscript {
pub(super) fn render_body(&self) -> String {
let mut line = format!("● {}{PADDED_INLINE_SEPARATOR}{}", self.label, self.activity);
if let Some(subagents) = &self.subagents {
for task in &subagents.tasks {
let branch = if subagents
.tasks
.last()
.is_some_and(|last| last.id == task.id)
{
"└─"
} else {
"├─"
};
let mut parts = vec![
task.label.clone(),
activity_status_text(task.status).to_string(),
];
if let Some(usage) = task.usage {
parts.push(compact_context_usage(usage));
}
line.push_str(&format!(
"\n {branch} {}",
parts.join(PADDED_INLINE_SEPARATOR)
));
for recent in &task.recent {
line.push_str(&format!("\n │ • {recent}"));
}
}
}
transcript::sanitize_preview(&line)
}
pub(super) fn render_entry(&self) -> transcript::TranscriptEntry {
transcript::TranscriptEntry::tool(
self.render_body(),
transcript::TranscriptToolData {
identity: transcript::TranscriptToolIdentity::Named {
name: self.tool_name.clone(),
status: Some(ActivityStatus::Running),
},
children: self
.subagents
.as_ref()
.map(|subagents| {
subagents
.tasks
.iter()
.map(|task| {
transcript::subagent_summary(&task.label, Some(task.status))
})
.collect()
})
.unwrap_or_default(),
},
)
}
}
impl ActiveSubagentTranscript {
pub(super) fn from_tool_call(parent_id: &ActivityId, arguments: &serde_json::Value) -> Self {
let tasks = arguments
.get("tasks")
.and_then(|value| value.as_array())
.map(|tasks| {
tasks
.iter()
.enumerate()
.map(|(index, task)| {
let task_key = format!("g{}", index + 1);
ActiveSubagentTask {
id: parent_id.child(&task_key),
label: task_label(&task_key, task),
status: ActivityStatus::Queued,
usage: None,
recent: VecDeque::new(),
}
})
.collect()
})
.unwrap_or_default();
Self { tasks }
}
pub(super) fn task_for_descendant_mut(
&mut self,
id: &ActivityId,
) -> Option<&mut ActiveSubagentTask> {
self.tasks
.iter_mut()
.find(|task| id == &task.id || id.is_path_descendant_of(&task.id))
}
pub(super) fn task_for_descendant(&self, id: &ActivityId) -> Option<&ActiveSubagentTask> {
self.tasks
.iter()
.find(|task| id == &task.id || id.is_path_descendant_of(&task.id))
}
pub(super) fn task_id_from_prefixed_descendant(
&self,
parent_id: &ActivityId,
id: &ActivityId,
) -> Option<ActivityId> {
let task_key = id
.path_suffix_after(parent_id)?
.split('/')
.next()
.filter(|key| !key.is_empty())?;
Some(parent_id.child(task_key))
}
pub(super) fn ensure_task_from_descendant(&mut self, parent_id: &ActivityId, id: &ActivityId) {
let Some(task_key) = id
.path_suffix_after(parent_id)
.and_then(|rest| rest.split('/').next().filter(|key| !key.is_empty()))
else {
return;
};
let task_id = parent_id.child(task_key);
if self.tasks.iter().any(|task| task.id == task_id) {
return;
}
self.tasks.push(ActiveSubagentTask {
id: task_id,
label: bounded_transcript_label(task_key, 80),
status: ActivityStatus::Queued,
usage: None,
recent: VecDeque::new(),
});
}
}
impl ActiveSubagentTask {
pub(super) fn push_recent(&mut self, summary: impl AsRef<str>) {
let summary = transcript::sanitize_preview(summary.as_ref());
if summary.trim().is_empty() {
return;
}
self.recent.push_back(summary);
while self.recent.len() > 3 {
self.recent.pop_front();
}
}
pub(super) fn update_usage(
&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.usage = Some(merge_context_usage(self.usage, incoming));
}
}
fn compact_context_usage(usage: ContextUsageState) -> String {
let percent = usage
.current_tokens
.saturating_mul(100)
.checked_div(usage.max_tokens)
.map_or("--".to_string(), |percent| percent.to_string());
let mut parts = vec![format!(
"ctx {percent}%/{}",
compact_token_limit(usage.max_tokens)
)];
if let Some(reasoning_tokens) = usage.reasoning_tokens {
parts.push(format!("thinking {reasoning_tokens}"));
}
parts.join(PADDED_INLINE_SEPARATOR)
}
fn compact_token_limit(tokens: usize) -> String {
if tokens < 1_000 {
return tokens.to_string();
}
format!("{}k", tokens.saturating_add(500) / 1_000)
}
fn task_label(task_key: &str, task: &serde_json::Value) -> String {
let mut parts = vec![task_key.to_string()];
if let Some(identity) = task
.get("identity")
.and_then(|value| value.as_str())
.filter(|value| !value.trim().is_empty())
{
parts.push(format!("identity {identity}"));
}
if let Some(agent) = task
.get("agent")
.and_then(|value| value.as_str())
.filter(|value| !value.trim().is_empty())
{
parts.push(format!("agent {agent}"));
}
bounded_transcript_label(&parts.join(PADDED_INLINE_SEPARATOR), 80)
}
pub(super) fn bounded_transcript_label(value: &str, max_width: usize) -> String {
let sanitized = normalize_inline_separators(&transcript::sanitize_preview(value));
if UnicodeWidthStr::width(sanitized.as_str()) <= max_width {
return sanitized;
}
let ellipsis_width = UnicodeWidthStr::width("…");
let content_width = max_width.saturating_sub(ellipsis_width);
let mut truncated = String::new();
let mut width = 0usize;
for grapheme in sanitized.graphemes(true) {
let next_width = width.saturating_add(UnicodeWidthStr::width(grapheme));
if next_width > content_width {
break;
}
truncated.push_str(grapheme);
width = next_width;
}
truncated.push('…');
truncated
}
pub(super) fn activity_kind_text(kind: &ActivityKind) -> &'static str {
match kind {
ActivityKind::Tool => "tool",
ActivityKind::Hook => "hook",
ActivityKind::ProviderContextInjection => "provider context injection",
ActivityKind::SubagentBatch => "batch",
ActivityKind::SubagentTask => "subagent",
ActivityKind::Assistant => "assistant",
ActivityKind::Diagnostic => "diagnostic",
ActivityKind::Compaction => "compaction",
}
}
pub(super) fn activity_status_text(status: ActivityStatus) -> &'static str {
match status {
ActivityStatus::Queued => "queued",
ActivityStatus::Running => "running",
ActivityStatus::Writing => "writing",
ActivityStatus::Success => "completed",
ActivityStatus::Failed => "failed",
ActivityStatus::Canceled => "canceled",
}
}
fn is_active_status(status: ActivityStatus) -> bool {
matches!(
status,
ActivityStatus::Queued | ActivityStatus::Running | ActivityStatus::Writing
)
}
fn root_activity_text(status: ActivityStatus, metadata: &ActivityMetadata) -> String {
let mut parts = vec![activity_status_text(status).to_string()];
parts.extend(transcript_metadata_fields(&metadata.fields));
parts.join(PADDED_INLINE_SEPARATOR)
}
use super::MissionControlState;
use crate::tui::activity::{self, ActivityNode, ActivityParentProvenance};
use std::collections::HashSet;
impl MissionControlState {
pub(crate) fn apply_activity_event(&mut self, event: ActivityEvent) {
self.apply_activity_event_with_live_time(event, Some(std::time::Instant::now()));
}
pub(crate) fn apply_historical_activity_event(&mut self, event: ActivityEvent) {
self.apply_activity_event_with_live_time(event, None);
}
fn apply_activity_event_with_live_time(
&mut self,
event: ActivityEvent,
live_time: Option<std::time::Instant>,
) {
if let ActivityEvent::FastObservation {
provider_id,
model,
requested_service_tier,
outcome,
request_sequence,
run_order,
} = &event
{
self.accept_fast_observation(
provider_id,
model,
requested_service_tier,
outcome,
*request_sequence,
*run_order,
);
return;
}
if let ActivityEvent::ToolResultDetail { detail, .. } = &event {
self.session_files.record(
&detail.tool_name,
detail.status == ActivityStatus::Success,
&detail.metadata,
&[],
);
}
if let ActivityEvent::UsageSnapshot {
id,
usage,
request_sequence,
final_usage,
} = &event
&& id
.0
.starts_with(super::super::session_usage::USAGE_ACTIVITY_PREFIX)
{
self.session_usage
.observe(super::super::session_usage::SessionUsageRecord {
id: id.0.clone(),
usage: usage.whole_run,
request_sequence: *request_sequence,
final_usage: *final_usage,
});
self.refresh_session_cache_percent();
return;
}
let previous_visible_len = self.visible_node_count();
let previous_selected = self.selected_activity_id();
let was_following_activity_tail = previous_visible_len == 0 || self.activity_follow_tail;
let mut invalidate_visible_nodes = false;
let mut invalidate_selected_detail = false;
let activity_event_id = match &event {
ActivityEvent::Started { id, .. }
| ActivityEvent::Delta { id, .. }
| ActivityEvent::UsageUpdate { id, .. }
| ActivityEvent::UsageSnapshot { id, .. }
| ActivityEvent::ToolStartedDetail { id, .. }
| ActivityEvent::ToolMetadata { id, .. }
| ActivityEvent::ToolResultDetail { id, .. }
| ActivityEvent::FinalPreview { id, .. }
| ActivityEvent::Finished { id, .. } => id.clone(),
ActivityEvent::FastObservation { .. } => {
unreachable!("Fast activity was handled above")
}
};
let previous_transcript_activity_entries = match &event {
ActivityEvent::Started { id, .. } => self.transcript_activity_entries_for_event(id),
_ => Vec::new(),
};
match event {
ActivityEvent::Started {
id,
parent_id,
kind,
status,
metadata,
} => {
self.invalidate_subagent_viewer_tabs();
let new_node = !self.nodes.contains_key(&id);
invalidate_visible_nodes |= new_node;
invalidate_selected_detail |=
self.activity_update_affects_selected_detail(&previous_selected, &id);
let requested_parent_id = parent_id
.as_ref()
.filter(|parent_id| self.nodes.contains_key(*parent_id))
.cloned();
let incoming_parent_provenance = match &parent_id {
Some(parent_id) => ActivityParentProvenance::Declared(parent_id.clone()),
None => ActivityParentProvenance::ExplicitRoot,
};
let (previous_parent_id, previous_parent_provenance) = self
.nodes
.get(&id)
.map(|node| (node.parent_id.clone(), node.parent_provenance.clone()))
.unwrap_or((None, ActivityParentProvenance::Unavailable));
let parent_assignment_rejected = requested_parent_id
.as_ref()
.is_some_and(|parent_id| self.parent_assignment_would_cycle(&id, parent_id));
let (attached_parent_id, parent_provenance) = if parent_assignment_rejected {
if new_node {
(None, incoming_parent_provenance)
} else {
(previous_parent_id.clone(), previous_parent_provenance)
}
} else {
(requested_parent_id, incoming_parent_provenance)
};
if new_node {
self.attach(&id, attached_parent_id.as_ref());
self.expanded.insert(id.clone());
} else if previous_parent_id != attached_parent_id {
self.detach(&id, previous_parent_id.as_ref());
self.attach(&id, attached_parent_id.as_ref());
invalidate_visible_nodes = true;
}
let (preview, preview_truncated) = self
.nodes
.get(&id)
.map(|n| (n.preview.clone(), n.preview_truncated))
.unwrap_or_default();
let started_at = self
.nodes
.get(&id)
.map(|node| node.started_at)
.unwrap_or_else(chrono::Local::now);
let parent_for_transcript = attached_parent_id.clone();
let metadata_label = metadata.label.clone();
let root_activity = parent_for_transcript
.is_none()
.then(|| root_activity_text(status, &metadata));
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: attached_parent_id,
parent_provenance,
kind: kind.clone(),
status,
metadata,
started_at,
update_sequence: 0,
own_update_sequence: 0,
preview,
preview_truncated,
tool_detail: self.nodes.get(&id).and_then(|n| n.tool_detail.clone()),
hash_edit_tree_summary: if matches!(&kind, ActivityKind::Tool) {
self.nodes
.get(&id)
.and_then(|n| n.hash_edit_tree_summary.clone())
} else {
None
},
subagent_display: self
.nodes
.get(&id)
.map(|n| n.subagent_display.clone())
.unwrap_or_default(),
usage: self.nodes.get(&id).and_then(|n| n.usage),
last_known_cache_percent: self
.nodes
.get(&id)
.and_then(|n| n.last_known_cache_percent),
last_known_latest_cache_percent: self
.nodes
.get(&id)
.and_then(|n| n.last_known_latest_cache_percent),
},
);
invalidate_visible_nodes |= self.reattach_waiting_children(&id);
if let Some(parent_id) = parent_for_transcript.as_ref() {
self.update_tool_transcript(parent_id, &format!("current: {metadata_label}"));
} else if let Some(root_activity) = root_activity {
self.update_tool_transcript(&id, &root_activity);
}
if new_node {
self.update_subagent_transcript_started(&id, &kind, status, &metadata_label);
}
}
ActivityEvent::Delta { id, preview } => {
let new_node = !self.nodes.contains_key(&id);
invalidate_visible_nodes |= new_node;
invalidate_selected_detail |=
self.activity_update_affects_selected_detail(&previous_selected, &id);
if new_node {
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: ActivityParentProvenance::Unavailable,
kind: ActivityKind::Diagnostic,
status: ActivityStatus::Running,
metadata: ActivityMetadata::new(id.as_str()),
started_at: chrono::Local::now(),
update_sequence: 0,
own_update_sequence: 0,
preview: String::new(),
preview_truncated: false,
tool_detail: None,
hash_edit_tree_summary: None,
subagent_display: Default::default(),
usage: None,
last_known_cache_percent: None,
last_known_latest_cache_percent: None,
},
);
}
if let Some(node) = self.nodes.get_mut(&id) {
activity::append_preview(node, &preview);
}
}
ActivityEvent::UsageUpdate {
id,
current_tokens,
max_tokens,
reasoning_tokens,
source,
request_sequence,
} => {
if let Some(node) = self.nodes.get_mut(&id) {
node.subagent_display.context = Some(merge_context_usage(
node.subagent_display.context,
ContextUsageState {
current_tokens,
max_tokens,
reasoning_tokens,
source,
request_sequence,
},
));
}
self.update_subagent_transcript_usage(
&id,
current_tokens,
max_tokens,
reasoning_tokens,
source,
request_sequence,
);
}
ActivityEvent::UsageSnapshot { id, usage, .. } => {
if let Some(node) = self.nodes.get_mut(&id) {
if let Some(percent) = activity::cache_read_percent(usage.whole_run) {
node.last_known_cache_percent = Some(percent);
}
if let Some(percent) = usage.latest.and_then(activity::cache_read_percent) {
node.last_known_latest_cache_percent = Some(percent);
}
node.usage = Some(usage);
}
}
ActivityEvent::ToolMetadata { id, metadata } => {
invalidate_selected_detail |=
self.activity_update_affects_selected_detail(&previous_selected, &id);
if let Some(detail) = self
.nodes
.get_mut(&id)
.and_then(|node| node.tool_detail.as_mut())
&& let (Some(current), Some(update)) =
(detail.metadata.as_object_mut(), metadata.as_object())
{
current.extend(update.clone());
}
}
ActivityEvent::ToolStartedDetail { id, detail }
| ActivityEvent::ToolResultDetail { id, detail } => {
let new_node = !self.nodes.contains_key(&id);
invalidate_visible_nodes |= new_node;
invalidate_selected_detail |=
self.activity_update_affects_selected_detail(&previous_selected, &id);
if new_node {
self.attach(&id, None);
let hash_edit_tree_summary = activity::hash_edit_activity_tree_summary(&detail);
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: ActivityParentProvenance::Unavailable,
kind: ActivityKind::Tool,
status: detail.status,
metadata: ActivityMetadata::new(detail.label.as_ref()),
started_at: chrono::Local::now(),
update_sequence: 0,
own_update_sequence: 0,
preview: String::new(),
preview_truncated: false,
tool_detail: Some(detail),
hash_edit_tree_summary,
subagent_display: Default::default(),
usage: None,
last_known_cache_percent: None,
last_known_latest_cache_percent: None,
},
);
} else if let Some(node) = self.nodes.get_mut(&id)
&& node.status != ActivityStatus::Canceled
{
if node.kind == ActivityKind::Tool && node.hash_edit_tree_summary.is_none() {
node.hash_edit_tree_summary =
activity::hash_edit_activity_tree_summary(&detail);
}
node.status = detail.status;
node.tool_detail = Some(detail);
}
}
ActivityEvent::FinalPreview {
id,
preview,
metadata,
status,
} => {
let new_node = !self.nodes.contains_key(&id);
invalidate_visible_nodes |= new_node;
invalidate_selected_detail |=
self.activity_update_affects_selected_detail(&previous_selected, &id);
let metadata = metadata.unwrap_or_else(|| {
self.nodes
.get(&id)
.map(|node| node.metadata.clone())
.unwrap_or_else(|| ActivityMetadata::new(id.as_str()))
});
let status = status.unwrap_or_else(|| {
self.nodes
.get(&id)
.map(|node| node.status)
.unwrap_or(ActivityStatus::Success)
});
self.upsert_activity_final_preview(id.clone(), &preview, metadata.clone(), status);
self.aggregate_parent_status(&id);
self.update_subagent_transcript_final_preview(&id, &metadata, status);
}
ActivityEvent::Finished {
id,
status,
metadata,
} => {
let new_node = !self.nodes.contains_key(&id);
invalidate_visible_nodes |= new_node;
invalidate_selected_detail |=
self.activity_update_affects_selected_detail(&previous_selected, &id);
if new_node {
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: ActivityParentProvenance::Unavailable,
kind: ActivityKind::Diagnostic,
status,
metadata: metadata
.unwrap_or_else(|| ActivityMetadata::new(id.as_str())),
started_at: chrono::Local::now(),
update_sequence: 0,
own_update_sequence: 0,
preview: String::new(),
preview_truncated: false,
subagent_display: Default::default(),
usage: None,
last_known_cache_percent: None,
last_known_latest_cache_percent: None,
tool_detail: None,
hash_edit_tree_summary: None,
},
);
} else if let Some(node) = self.nodes.get_mut(&id)
&& node.status != ActivityStatus::Canceled
{
node.status = status;
if let Some(detail) = node.tool_detail.as_mut() {
detail.status = status;
}
if let Some(metadata) = metadata {
node.metadata = metadata;
}
}
self.aggregate_parent_status(&id);
if let Some(parent_id) = self.nodes.get(&id).and_then(|node| node.parent_id.clone())
{
let progress = self.activity_child_progress(&parent_id);
self.update_tool_transcript(&parent_id, &progress);
} else if let Some(metadata) = self.nodes.get(&id).map(|node| node.metadata.clone())
{
self.update_tool_transcript(&id, &root_activity_text(status, &metadata));
} else {
self.update_tool_transcript(&id, activity_status_text(status));
}
let metadata = self.nodes.get(&id).map(|node| node.metadata.clone());
self.update_subagent_transcript_finished(&id, status, metadata.as_ref());
}
ActivityEvent::FastObservation { .. } => {
unreachable!("Fast activity was handled above")
}
}
if let Some(node) = self.nodes.get_mut(&activity_event_id) {
if node.kind == ActivityKind::SubagentTask
&& let Some(now) = live_time
{
node.subagent_display.observe_status(node.status, now);
}
node.refresh_transcript_data();
}
self.mark_activity_updated(&activity_event_id);
invalidate_selected_detail |=
self.activity_update_affects_selected_detail(&previous_selected, &activity_event_id);
invalidate_selected_detail |= self.activity_update_affects_selected_detail(
&self.modals.activity_detail,
&activity_event_id,
);
if invalidate_visible_nodes {
self.invalidate_activity_visible_nodes_cache();
self.normalize_selection_after_visible_change(previous_selected.clone());
}
if invalidate_selected_detail {
self.invalidate_selected_activity_detail_cache();
}
self.invalidate_transcript_activity_caches_for_event(
&activity_event_id,
previous_transcript_activity_entries,
);
self.follow_activity_tail_after_append(previous_visible_len, was_following_activity_tail);
}
pub(crate) fn tick_subagent_elapsed(&mut self, now: std::time::Instant) -> bool {
let mut changed = Vec::new();
for (id, node) in &mut self.nodes {
if node.kind != ActivityKind::SubagentTask
|| node.subagent_display.finished
|| node.subagent_display.started.is_none()
{
continue;
}
let previous = node.subagent_display.elapsed.as_secs();
node.subagent_display.observe_status(node.status, now);
if node.subagent_display.elapsed.as_secs() != previous {
changed.push(id.clone());
}
}
for id in &changed {
self.mark_activity_updated(id);
self.invalidate_transcript_activity_caches_for_event(id, Vec::new());
}
!changed.is_empty()
}
fn mark_activity_updated(&mut self, id: &ActivityId) {
self.activity_update_sequence = self.activity_update_sequence.saturating_add(1);
let sequence = self.activity_update_sequence;
if let Some(node) = self.nodes.get_mut(id) {
node.own_update_sequence = sequence;
}
let mut affected = HashSet::from([id.clone()]);
let mut ancestor = self.nodes.get(id).and_then(|node| node.parent_id.clone());
while let Some(parent_id) = ancestor {
if !affected.insert(parent_id.clone()) {
break;
}
ancestor = self
.nodes
.get(&parent_id)
.and_then(|node| node.parent_id.clone());
}
let use_path_fallback = self.nodes.get(id).is_none_or(|node| {
node.parent_id.is_none()
&& matches!(
node.parent_provenance,
ActivityParentProvenance::Unavailable
)
});
if use_path_fallback {
for (index, _) in id.as_str().match_indices('/') {
let ancestor = ActivityId::new(&id.as_str()[..index]);
if self
.nodes
.get(&ancestor)
.is_some_and(|node| node.kind == ActivityKind::SubagentTask)
{
affected.insert(ancestor);
}
}
}
for affected_id in affected {
if let Some(node) = self.nodes.get_mut(&affected_id) {
node.update_sequence = sequence;
}
}
}
fn activity_update_affects_selected_detail(
&self,
selected: &Option<ActivityId>,
id: &ActivityId,
) -> bool {
let Some(selected) = selected else {
return false;
};
selected == id || self.activity_is_descendant_of(id, selected)
}
fn transcript_activity_entries_for_event(&self, id: &ActivityId) -> Vec<usize> {
if self.transcript.activity_links().is_empty() {
return Vec::new();
}
self.transcript
.activity_links()
.iter()
.filter_map(|(index, link)| {
self.activity_event_affects_transcript_link(id, &link.activity_id)
.then(|| self.transcript_local_index(index.index()))
.flatten()
})
.collect()
}
fn invalidate_transcript_activity_caches_for_event(
&self,
id: &ActivityId,
previous_entries: Vec<usize>,
) {
let current_entries = self.transcript_activity_entries_for_event(id);
if previous_entries.is_empty() {
self.invalidate_transcript_entries_cache(current_entries);
return;
}
let mut affected_entries = previous_entries;
affected_entries.extend(current_entries);
affected_entries.sort_unstable();
affected_entries.dedup();
self.invalidate_transcript_entries_cache(affected_entries);
}
fn activity_event_affects_transcript_link(
&self,
event_id: &ActivityId,
linked_id: &ActivityId,
) -> bool {
event_id == linked_id
|| self.activity_is_descendant_of(event_id, linked_id)
|| self.activity_is_descendant_of(linked_id, event_id)
}
fn follow_activity_tail_after_append(
&mut self,
previous_visible_len: usize,
was_following_activity_tail: bool,
) {
if !was_following_activity_tail {
return;
}
let visible_len = self.visible_node_count();
if visible_len == 0 {
self.selected = 0;
self.set_scroll_offset(&self.scroll_views.activity_tree, 0);
return;
}
let tail = visible_len.saturating_sub(1);
if visible_len <= previous_visible_len && self.selected == tail {
return;
}
let previous = self.selected;
self.selected = tail;
if previous_visible_len > 0 {
self.set_scroll_offset(&self.scroll_views.activity_tree, usize::MAX);
}
self.activity_follow_tail = true;
if self.selected != previous {
self.reset_detail();
}
}
}