1use crate::types::{ArtifactName, EvalId};
3use serde_json::Value;
4use time::OffsetDateTime;
5
6use super::{
7 Saved,
8 states::{
9 Activity, Busy, Completion, EvalView, NotRun, Queue, Rank, Source, States, Upstream, Waits,
10 },
11};
12use crate::store::{RequestView, RunView};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Tone {
17 Green,
18 Red,
19 Error,
20 Running,
21 Human,
22 Queued,
23 Blocked,
24 Muted,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum Weight {
30 Bold,
31 Normal,
32 Dim,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct Segment {
38 pub text: String,
39 pub tone: Option<Tone>,
40}
41
42impl Segment {
43 fn plain(text: impl Into<String>) -> Self {
44 Self {
45 text: text.into(),
46 tone: None,
47 }
48 }
49 fn toned(text: impl Into<String>, tone: Tone) -> Self {
50 Self {
51 text: text.into(),
52 tone: Some(tone),
53 }
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum Kind {
59 Artifact {
61 completion: Completion,
62 basis: bool,
63 },
64 Eval(EvalView),
65}
66
67#[derive(Debug, Clone)]
68pub struct Node {
69 pub id: super::NodeId,
71 pub kind: Kind,
72 pub glyph: &'static str,
73 pub tone: Tone,
74 pub weight: Weight,
75 pub name: String,
76 pub marks: String,
78 pub text: Vec<Segment>,
79 pub right: String,
81 pub compact: String,
83 pub changed: bool,
85 pub upstream: Vec<Upstream>,
87 pub dependency: bool,
89 pub clock: Option<Clock>,
91 pub children: Vec<Node>,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub struct Clock {
97 pub start: OffsetDateTime,
98 pub end: Option<OffsetDateTime>,
99}
100
101impl Clock {
102 pub fn elapsed(self, now: OffsetDateTime) -> String {
103 super::duration((self.end.unwrap_or(now) - self.start).whole_seconds())
104 }
105}
106
107impl Node {
108 pub fn right_at(&self, now: OffsetDateTime, compact: bool) -> String {
110 match self.clock {
111 Some(clock) => clock.elapsed(now),
112 None if compact => self.compact.clone(),
113 None => self.right.clone(),
114 }
115 }
116
117 pub fn status(&self) -> String {
119 self.text
120 .iter()
121 .map(|segment| segment.text.as_str())
122 .collect()
123 }
124
125 pub fn line(&self) -> String {
127 let mut line = format!("{} {}{}", self.glyph, self.name, self.marks);
128 for part in [self.status(), self.right.clone()] {
129 if !part.is_empty() {
130 line = format!("{line} {part}");
131 }
132 }
133 if self.changed {
134 line.push_str(" *");
135 }
136 line
137 }
138
139 pub fn done(&self) -> bool {
141 matches!(
142 self.kind,
143 Kind::Artifact {
144 completion: Completion::Complete,
145 ..
146 }
147 ) && !self.children.is_empty()
148 }
149
150 pub fn urgent(&self) -> bool {
152 matches!(
153 self.kind,
154 Kind::Eval(EvalView::Failed { .. } | EvalView::InProgress(Activity::Human(_)))
155 )
156 }
157
158 pub fn attention(&self) -> bool {
160 matches!(
161 self.kind,
162 Kind::Eval(EvalView::InProgress(_) | EvalView::Failed { .. })
163 )
164 }
165}
166
167pub fn completion(completion: Completion) -> (&'static str, Tone, &'static str) {
169 match completion {
170 Completion::Complete => ("✓", Tone::Green, "complete"),
171 Completion::InProgress(Busy::Running) => ("◐", Tone::Running, "in progress"),
172 Completion::InProgress(Busy::Human) => ("?", Tone::Human, "in progress"),
173 Completion::InProgress(Busy::Queued) => ("·", Tone::Queued, "in progress"),
174 Completion::Waiting { blocked: false } => ("…", Tone::Muted, "waiting"),
175 Completion::Waiting { blocked: true } => ("⊘", Tone::Blocked, "blocked"),
176 Completion::Failed { verdict: true } => ("✗", Tone::Red, "failed"),
177 Completion::Failed { verdict: false } => ("!", Tone::Error, "failed"),
178 }
179}
180
181fn style(view: &EvalView) -> (&'static str, Tone, Weight) {
183 match view {
184 EvalView::Done(_) => ("✓", Tone::Green, Weight::Normal),
185 EvalView::Failed { verdict: true } => ("✗", Tone::Red, Weight::Normal),
186 EvalView::Failed { verdict: false } => ("!", Tone::Error, Weight::Normal),
187 EvalView::InProgress(Activity::Running) => ("◐", Tone::Running, Weight::Bold),
188 EvalView::InProgress(Activity::Human(_)) => ("?", Tone::Human, Weight::Bold),
189 EvalView::InProgress(Activity::Queued(_)) => ("·", Tone::Queued, Weight::Normal),
190 EvalView::WaitingOn(_) => ("…", Tone::Muted, Weight::Dim),
191 EvalView::BlockedBy(_) => ("⊘", Tone::Blocked, Weight::Normal),
192 EvalView::NotRun(NotRun::Budget) => ("$", Tone::Blocked, Weight::Normal),
193 EvalView::NotRun(NotRun::Absent) => ("-", Tone::Muted, Weight::Dim),
194 EvalView::NotRun(_) => ("○", Tone::Muted, Weight::Dim),
195 }
196}
197
198const SHOWN: usize = 2;
200const SHOWN_TEXT: usize = 24;
203
204fn tokens(states: &States, x: &[crate::types::ArtifactName], glyphs: bool) -> Vec<Segment> {
206 let mut segments = Vec::new();
207 for (index, id) in x.iter().take(SHOWN).enumerate() {
208 if index > 0 {
209 segments.push(Segment::plain(", "));
210 }
211 let (glyph, tone, word) = completion(states.completion(id));
212 let text = match (glyphs, x.len()) {
213 (false, _) => id.to_string(),
214 (true, 1) => format!("{id} {glyph} {word}"),
215 (true, _) => format!("{id} {glyph}"),
216 };
217 segments.push(Segment::toned(text, tone));
218 }
219 if x.len() > SHOWN {
220 segments.push(Segment::plain(format!(" +{}", x.len() - SHOWN)));
221 }
222 if glyphs
223 && let Some(failed) = x
224 .iter()
225 .find(|id| states.completion(id) == Completion::Failed { verdict: false })
226 {
227 segments.push(Segment::plain(format!(" (retry {failed} first)")));
228 }
229 segments
230}
231
232fn waits(states: &States, prefix: &str, waits: &Waits) -> Vec<Segment> {
233 let mut segments = vec![Segment::plain(prefix)];
234 segments.extend(tokens(states, &waits.artifacts, true));
235 if let Some((via, root)) = &waits.root {
236 segments.push(Segment::plain(format!(" ({via} waits for ")));
237 let (_, tone, _) = completion(states.completion(root));
238 segments.push(Segment::toned(root.clone(), tone));
239 segments.push(Segment::plain(")"));
240 }
241 segments
242}
243
244fn findings(result: Option<&crate::store::ExecutionResult>) -> String {
246 let result = result.map(crate::store::ExecutionResult::to_json);
247 let fields = result
248 .as_ref()
249 .and_then(Value::as_object)
250 .into_iter()
251 .flatten();
252 let fields = fields.filter(|(key, _)| {
253 !matches!(
254 key.as_str(),
255 "verdict" | "stdout" | "stderr" | "truncated" | "derived" | "durationMs"
256 )
257 });
258 let parts: Vec<_> = fields
259 .take(SHOWN)
260 .map(|(key, value)| match value {
261 Value::Array(items) => format!("{key} {}", items.len()),
262 Value::Object(fields) => format!("{key} {{{}}}", fields.len()),
263 Value::String(text) => {
264 let text = text.lines().next().unwrap_or_default();
265 let mut short: String = text.chars().take(SHOWN_TEXT).collect();
266 if short.len() < text.len() {
267 short.push('…');
268 }
269 format!("{key} {short}")
270 }
271 value => format!("{key} {value}"),
272 })
273 .collect();
274 parts.join(", ")
275}
276
277fn short_profile(profile: &crate::config::StoredProfile) -> String {
278 use crate::config::StoredProfile;
279 match profile {
280 StoredProfile::Agent { backend, model, .. } => format!(
281 "agent {} {model}",
282 serde_json::to_value(backend)
283 .ok()
284 .and_then(|value| value.as_str().map(str::to_owned))
285 .unwrap_or_default()
286 ),
287 StoredProfile::Runtime { command, .. } => format!("runtime {command}"),
288 StoredProfile::Human {} => "human".into(),
289 StoredProfile::Dependency { .. } => "dependency".into(),
290 }
291}
292
293fn text(states: &States, view: &EvalView, request: Option<&RequestView>) -> Vec<Segment> {
294 let plain = |text: &str| vec![Segment::plain(text)];
295 match view {
296 EvalView::Done(Source::Executed) => plain("GREEN"),
297 EvalView::Done(Source::Reused) => plain("GREEN · reused"),
298 EvalView::Done(Source::Derived) => plain("GREEN · derived"),
299 EvalView::Done(Source::Saved) => plain("GREEN · saved result, not in this Run"),
300 EvalView::Failed { verdict: true } => {
301 let findings = findings(request.and_then(|view| view.request.result()));
302 if request.is_none() {
303 plain("RED · saved result, not in this Run")
304 } else if findings.is_empty() {
305 plain("RED")
306 } else {
307 vec![Segment::plain(format!("RED · {findings}"))]
308 }
309 }
310 EvalView::Failed { verdict: false } => match request.and_then(super::error) {
311 Some(error) => vec![Segment::plain(format!(
312 "ERROR · {}",
313 error.lines().next().unwrap_or_default()
314 ))],
315 None if request.is_none() => plain("ERROR · saved result, not in this Run"),
316 None => plain("ERROR"),
317 },
318 EvalView::InProgress(Activity::Running) => vec![Segment::plain(format!(
319 "running · {}",
320 request.map_or_else(String::new, |view| short_profile(&view.request.profile))
321 ))],
322 EvalView::InProgress(Activity::Human(None)) => plain("Human sign-off · unclaimed"),
323 EvalView::InProgress(Activity::Human(Some(reviewer))) => vec![Segment::plain(format!(
324 "Human sign-off · claimed by {reviewer}"
325 ))],
326 EvalView::InProgress(Activity::Queued(Queue::Jobs)) => plain("queued"),
327 EvalView::InProgress(Activity::Queued(Queue::Slot(backend))) => {
328 vec![Segment::plain(format!("queued · {backend} slots full"))]
329 }
330 EvalView::InProgress(Activity::Queued(Queue::Joined)) => {
331 plain("queued · joins the running review")
332 }
333 EvalView::WaitingOn(x) => waits(states, "waits for ", x),
334 EvalView::BlockedBy(x) => {
335 let mut segments = vec![Segment::plain(if states.running {
336 "blocked by "
337 } else {
338 "not run: blocked by "
339 })];
340 segments.extend(tokens(states, x, true));
341 segments
342 }
343 EvalView::NotRun(NotRun::Dependency(x)) => waits(states, "not run: waited for ", x),
344 EvalView::NotRun(NotRun::Budget) => plain("not started · maxExecutions reached"),
345 EvalView::NotRun(NotRun::Stale) => plain("not run · no reusable result"),
346 EvalView::NotRun(NotRun::Unreviewed) => plain("not reviewed"),
347 EvalView::NotRun(NotRun::Absent) => plain("not in this Run"),
348 }
349}
350
351fn clock(view: &EvalView, request: Option<&RequestView>) -> Option<Clock> {
353 let timed = matches!(
354 view,
355 EvalView::Done(Source::Executed)
356 | EvalView::Failed { .. }
357 | EvalView::InProgress(Activity::Running | Activity::Human(_))
358 );
359 let request = &request.filter(|_| timed)?.request;
360 let start = request.started_at.unwrap_or(request.created_at).time();
361 let end = request.completed_at().map(crate::types::Timestamp::time);
362 Some(Clock { start, end })
363}
364
365fn compact(view: &EvalView, right: &str) -> String {
366 let x = match view {
367 EvalView::WaitingOn(waits) | EvalView::NotRun(NotRun::Dependency(waits)) => {
368 &waits.artifacts
369 }
370 EvalView::BlockedBy(x) => x,
371 _ => return right.to_owned(),
372 };
373 match x.as_slice() {
374 [one] => format!("←{one}"),
375 many => format!("←{}", many.len()),
376 }
377}
378
379fn local(eval: &EvalId) -> &str {
380 eval.rsplit_once('/')
381 .map_or(eval.as_str(), |(_, local)| local)
382}
383
384fn eval_node<'a>(states: &States<'a>, eval: &'a EvalId, now: OffsetDateTime) -> Node {
385 let view = states.view(eval);
386 let request = states.request(eval);
387 let (glyph, tone, weight) = style(&view);
388 let clock = clock(&view, request);
389 let right = clock.map_or_else(String::new, |clock| clock.elapsed(now));
390 Node {
391 clock,
392 id: super::NodeId::eval(eval.clone()),
393 glyph,
394 tone,
395 weight,
396 name: local(eval).to_owned(),
397 dependency: states.index.dependency.contains(eval),
398 marks: if states.index.dependency.contains(eval) {
399 " [dep]".into()
400 } else {
401 String::new()
402 },
403 text: text(states, &view, request),
404 compact: compact(&view, &right),
405 right,
406 changed: false,
407 upstream: states.upstream_states(eval),
408 children: Vec::new(),
409 kind: Kind::Eval(view),
410 }
411}
412
413fn names(nodes: &[&Node]) -> String {
415 let mut names = nodes
416 .iter()
417 .take(SHOWN)
418 .map(|node| node.name.as_str())
419 .collect::<Vec<_>>()
420 .join(", ");
421 if nodes.len() > SHOWN {
422 names = format!("{names} +{}", nodes.len() - SHOWN);
423 }
424 names
425}
426
427fn summary(states: &States, id: &ArtifactName, children: &[Node]) -> Vec<Segment> {
429 let view = |node: &Node| match &node.kind {
430 Kind::Eval(view) => Some(view.clone()),
431 Kind::Artifact { .. } => None,
432 };
433 let Some(top) = children
434 .iter()
435 .filter_map(view)
436 .map(|view| view.rank())
437 .min()
438 else {
439 return Vec::new();
440 };
441 let class = |rank: Rank| -> Vec<&Node> {
442 children
443 .iter()
444 .filter(|node| view(node).is_some_and(|view| view.rank() == rank))
445 .collect()
446 };
447 let x = |ids: Vec<crate::types::ArtifactName>| tokens(states, &ids, false);
448 let mut segments = match top {
449 Rank::Failed => {
450 let failed = class(Rank::Failed);
451 let errors: Vec<_> = failed
452 .iter()
453 .copied()
454 .filter(|node| node.kind == Kind::Eval(EvalView::Failed { verdict: false }))
455 .collect();
456 let (label, nodes) = if errors.is_empty() {
457 ("RED", failed)
458 } else {
459 ("ERROR", errors)
460 };
461 vec![Segment::plain(format!("{label}: {}", names(&nodes)))]
462 }
463 Rank::InProgress => vec![Segment::plain(format!(
464 "in progress: {}",
465 names(&class(Rank::InProgress))
466 ))],
467 Rank::Blocked => {
468 let mut segments = vec![Segment::plain(if states.running {
469 "blocked by "
470 } else {
471 "not run: blocked by "
472 })];
473 segments.extend(x(states.blockers_of(id)));
474 segments
475 }
476 Rank::Waiting => {
477 let mut segments = vec![Segment::plain("waits for ")];
478 segments.extend(x(states.waits_of(id)));
479 segments
480 }
481 Rank::NotRun => {
482 let first = class(Rank::NotRun)[0];
483 match &first.kind {
484 Kind::Eval(EvalView::NotRun(NotRun::Dependency(_))) => {
485 let mut segments = vec![Segment::plain("not run: waited for ")];
486 segments.extend(x(states.waits_of(id)));
487 segments
488 }
489 _ => first.text.clone(),
490 }
491 }
492 Rank::Done => match states.completion(id) {
494 Completion::Waiting { blocked: true } => {
495 let mut segments = vec![Segment::plain("done, but blocked by ")];
496 segments.extend(x(states.blockers_of(id)));
497 segments
498 }
499 Completion::Waiting { blocked: false } => {
500 let mut segments = vec![Segment::plain("done, but waits for ")];
501 segments.extend(x(states.waits_of(id)));
502 segments
503 }
504 _ => Vec::new(),
505 },
506 };
507 if top == Rank::Failed {
508 let busy = class(Rank::InProgress);
509 if !busy.is_empty() {
510 segments.push(Segment::plain(format!(" · in progress: {}", names(&busy))));
511 }
512 }
513 segments
514}
515
516fn artifact_node<'a>(
517 states: &States<'a>,
518 saved: &Saved<'a>,
519 id: &ArtifactName,
520 now: OffsetDateTime,
521) -> Node {
522 let artifact = saved.artifact(id);
523 let basis = artifact.and_then(|artifact| artifact.basis.value()) == Some(&true);
524 let children: Vec<_> = states
525 .evals(id)
526 .iter()
527 .map(|&eval| eval_node(states, eval, now))
528 .collect();
529 let mut marks = String::new();
530 if states.index.cyclic.contains(id) {
531 let peers = states.index.peers.get(id).map_or(&[][..], Vec::as_slice);
532 marks = match peers {
533 [] => " ↻".into(),
534 [peer] => format!(" ↻ {peer}"),
535 [peer, rest @ ..] => format!(" ↻ {peer} +{}", rest.len()),
536 };
537 }
538 if artifact.and_then(|artifact| artifact.kind.value())
539 == Some(&crate::config::ArtifactKind::File)
540 {
541 marks.push_str(" [file]");
542 }
543 let completion = states.completion(id);
544 let passed = states
546 .evals(id)
547 .iter()
548 .filter(|eval| states.effective(eval) == crate::graph::EvalStatus::Green)
549 .count();
550 let right = if children.is_empty() {
551 String::new()
552 } else {
553 format!("{passed}/{}", children.len())
554 };
555 let (glyph, tone, weight, text) = if basis {
556 ("◇", Tone::Muted, Weight::Dim, vec![Segment::plain("basis")])
557 } else if children.is_empty() {
558 (
559 "○",
560 Tone::Muted,
561 Weight::Dim,
562 vec![Segment::plain("no evals")],
563 )
564 } else {
565 let top = children
567 .iter()
568 .min_by_key(|node| match &node.kind {
569 Kind::Eval(EvalView::Failed { verdict: false }) => (Rank::Failed, false),
570 Kind::Eval(view) => (view.rank(), true),
571 Kind::Artifact { .. } => (Rank::Done, true),
572 })
573 .expect("children");
574 let (glyph, tone, weight) = match (&top.kind, completion) {
575 (_, Completion::InProgress(busy)) => {
576 let (glyph, tone, _) = self::completion(Completion::InProgress(busy));
577 (glyph, tone, Weight::Bold)
578 }
579 (Kind::Eval(EvalView::Done(_)), Completion::Waiting { blocked }) => {
581 let (glyph, tone, _) = self::completion(Completion::Waiting { blocked });
582 let weight = if blocked { Weight::Normal } else { Weight::Dim };
583 (glyph, tone, weight)
584 }
585 _ => (top.glyph, top.tone, top.weight),
586 };
587 (glyph, tone, weight, summary(states, id, &children))
588 };
589 Node {
590 id: super::NodeId::artifact(id.clone()),
591 clock: None,
592 kind: Kind::Artifact { completion, basis },
593 glyph,
594 tone,
595 weight,
596 name: id.to_string(),
597 marks,
598 text,
599 compact: right.clone(),
600 right,
601 changed: false,
602 upstream: Vec::new(),
603 dependency: false,
604 children,
605 }
606}
607
608pub fn tree(run: &RunView, requests: &[RequestView], now: OffsetDateTime) -> Vec<Node> {
611 let mut nodes = build(run, requests, now);
612 if let Some(ended) = at_end(run, requests) {
615 mark(&mut nodes, &build(run, &ended, now));
616 }
617 nodes
618}
619
620fn build(run: &RunView, requests: &[RequestView], now: OffsetDateTime) -> Vec<Node> {
621 let saved = Saved { run, requests };
622 let states = States::new(&saved);
623 states
624 .index
625 .order
626 .iter()
627 .map(|id| artifact_node(&states, &saved, id, now))
628 .collect()
629}
630
631fn at_end(run: &RunView, requests: &[RequestView]) -> Option<Vec<RequestView>> {
635 let end = run.run.completed_at()?;
636 let late = |time: Option<crate::types::Timestamp>| time.is_some_and(|time| time > end);
637 let changed = requests.iter().any(|view| {
638 late(view.request.completed_at()) || late(view.claim.as_ref().map(|claim| claim.claimed_at))
639 });
640 if !changed {
641 return None;
642 }
643 let mut ended = requests.to_vec();
644 for view in &mut ended {
645 if late(view.claim.as_ref().map(|claim| claim.claimed_at)) {
646 view.claim = None;
647 }
648 let request = &mut view.request;
649 if late(request.completed_at()) {
650 request.state = if request.profile.kind() == crate::config::ProfileKind::Human {
651 crate::store::RequestState::WaitingHuman
652 } else {
653 crate::store::RequestState::Running
654 };
655 }
656 }
657 Some(ended)
658}
659
660fn state(node: &Node) -> (Kind, &'static str, String) {
662 let kind = match &node.kind {
663 Kind::Eval(EvalView::WaitingOn(waits)) => Kind::Eval(EvalView::WaitingOn(Waits {
664 artifacts: waits.artifacts.clone(),
665 root: None,
666 })),
667 Kind::Eval(EvalView::NotRun(NotRun::Dependency(waits))) => {
668 Kind::Eval(EvalView::NotRun(NotRun::Dependency(Waits {
669 artifacts: waits.artifacts.clone(),
670 root: None,
671 })))
672 }
673 kind => kind.clone(),
674 };
675 let summary = match node.kind {
677 Kind::Artifact { .. } => format!("{} {}", node.status(), node.right),
678 Kind::Eval(_) => String::new(),
679 };
680 (kind, node.glyph, summary)
681}
682
683fn mark(nodes: &mut [Node], ended: &[Node]) {
685 let mut before = std::collections::HashMap::new();
686 for node in ended {
687 before.insert(&node.id, state(node));
688 for child in &node.children {
689 before.insert(&child.id, state(child));
690 }
691 }
692 for node in nodes {
693 node.changed = before.get(&node.id) != Some(&state(node));
694 for child in &mut node.children {
695 child.changed = before.get(&child.id) != Some(&state(child));
696 }
697 }
698}
699
700pub fn upstream_index(run: &RunView, requests: &[RequestView]) -> Vec<(EvalId, Vec<ArtifactName>)> {
702 let saved = Saved { run, requests };
703 let index = super::states::Index::new(&saved);
704 index
705 .upstream
706 .iter()
707 .map(|(eval, artifacts)| {
708 (
709 (*eval).clone(),
710 artifacts.iter().map(|id| (*id).clone()).collect(),
711 )
712 })
713 .collect()
714}
715
716pub(super) fn artifact_status(saved: &Saved, id: &ArtifactName, now: OffsetDateTime) -> String {
718 let states = States::new(saved);
719 let node = artifact_node(&states, saved, id, now);
720 let (_, _, word) = completion(states.completion(id));
721 let status = node.status();
722 let status = if status.is_empty() {
723 word.to_owned()
724 } else {
725 status
726 };
727 if node.right.is_empty() {
728 format!("{} {status}", node.glyph)
729 } else {
730 format!("{} {status} · {} Evals GREEN", node.glyph, node.right)
731 }
732}
733
734pub(super) fn waits_for(saved: &Saved, eval: &EvalId, now: OffsetDateTime) -> String {
737 let states = States::new(saved);
738 let Some((&target, _)) = states
739 .index
740 .evals
741 .iter()
742 .find(|(_, evals)| evals.contains(&eval))
743 else {
744 return String::new();
745 };
746 let peers = |id: &ArtifactName| states.index.peers.get(id).cloned().unwrap_or_default();
747 let mut own = peers(target);
748 own.push(target);
749 let mut lines = Vec::new();
750 for upstream in states.upstream_states(eval) {
751 let (glyph, _, word) = completion(upstream.completion);
752 let id = &upstream.artifact;
753 let mut origins: Vec<String> = saved
754 .relations(id, false)
755 .into_iter()
756 .filter(|relation| own.contains(&&relation.target))
757 .map(|relation| {
758 let kind = super::relation_kind(relation);
759 if &relation.target == target {
760 kind
761 } else {
762 format!("{kind} → {}", relation.target)
763 }
764 })
765 .collect();
766 if origins.is_empty() && !peers(id).is_empty() {
767 origins.push(format!("↻ peer of {}", super::join(peers(id), ", ")));
768 }
769 let origins = if origins.is_empty() {
770 String::new()
771 } else {
772 format!(" · {}", origins.join(", "))
773 };
774 lines.push(format!("↑ {id} {glyph} {word}{origins}"));
775 if upstream.completion == Completion::Complete {
776 continue;
777 }
778 for &pending in states.evals(id) {
779 let node = eval_node(&states, pending, now);
780 if !matches!(node.kind, Kind::Eval(EvalView::Done(_))) {
781 lines.push(format!(" {} {} {}", node.glyph, node.name, node.status()));
782 }
783 }
784 }
785 lines.join("\n")
786}