Skip to main content

basis_tasks/
live.rs

1//! A secondary sink for a task's events, alongside the durable journal.
2//!
3//! Every attach writes every event to `events.jsonl` regardless — that is the
4//! durable record `watch` and a later `wait` read back. Showing a run *live*,
5//! on top of that, is a property of who asked for it rather than of the task
6//! itself: a shell blocked on `basis wait` wants to watch, a host polling
7//! `--json` wants only the settled object, and a child driven incidentally by
8//! its parent's settle pass was not asked for at all. So it is supplied per
9//! call, through [`DriveContext`], rather than fixed on [`Tasks`](crate::Tasks).
10
11use std::sync::Arc;
12
13use serde_json::Value;
14
15use crate::approve::PromptHost;
16
17/// Shown a task's events as they are journaled. `basis-cli`'s own terminal
18/// renderer is the first implementation, but the trait carries no opinion
19/// about a terminal — a host could log, forward over a socket, or update a
20/// UI just as well.
21pub trait LiveSink: Send + Sync {
22    fn on_event(&self, event: &Value);
23}
24
25/// What a process attaching to drive a task brings to it: how to show
26/// progress live, and how to answer `Approve::Prompt`. The two are
27/// independent — a child driven quietly by its parent's settle pass still
28/// answers prompts through the same host the parent would, it simply is not
29/// shown (see [`hidden`](Self::hidden)).
30#[derive(Clone, Default)]
31pub(crate) struct DriveContext {
32    pub(crate) live: Option<Arc<dyn LiveSink>>,
33    pub(crate) prompt_host: Option<Arc<dyn PromptHost>>,
34}
35
36impl DriveContext {
37    pub(crate) fn new(
38        live: Option<Arc<dyn LiveSink>>,
39        prompt_host: Option<Arc<dyn PromptHost>>,
40    ) -> Self {
41        Self { live, prompt_host }
42    }
43
44    /// This context, with progress silenced but the same say over prompts —
45    /// what a child driven by its parent's settle pass gets: nobody asked to
46    /// watch it, but it is still this process answering for it.
47    pub(crate) fn hidden(&self) -> Self {
48        Self {
49            live: None,
50            prompt_host: self.prompt_host.clone(),
51        }
52    }
53
54    pub(crate) fn show(&self, event: &Value) {
55        if let Some(live) = &self.live {
56            live.on_event(event);
57        }
58    }
59
60    pub(crate) fn can_ask(&self) -> bool {
61        self.prompt_host.as_deref().is_some_and(PromptHost::can_ask)
62    }
63
64    pub(crate) fn approver(&self) -> Option<Box<dyn basis::Approver>> {
65        self.prompt_host.as_deref().map(PromptHost::approver)
66    }
67}