Skip to main content

opal/ui/
handle.rs

1use super::runner::UiRunner;
2use super::types::{UiCommand, UiEvent, UiJobInfo, UiJobResources, UiJobStatus};
3use crate::history::HistoryEntry;
4use anyhow::Result;
5use std::collections::HashMap;
6use std::path::PathBuf;
7use std::sync::Mutex;
8use std::thread;
9use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
10
11pub struct UiHandle {
12    sender: UnboundedSender<UiEvent>,
13    command_rx: Mutex<Option<UnboundedReceiver<UiCommand>>>,
14    thread: thread::JoinHandle<()>,
15}
16
17#[derive(Clone)]
18pub struct UiBridge {
19    sender: UnboundedSender<UiEvent>,
20}
21
22impl UiHandle {
23    pub fn start(
24        jobs: Vec<UiJobInfo>,
25        history: Vec<HistoryEntry>,
26        current_run_id: String,
27        job_resources: HashMap<String, UiJobResources>,
28        plan_text: String,
29        workdir: PathBuf,
30        pipeline_path: PathBuf,
31    ) -> Result<Self> {
32        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
33        let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
34        let thread_tx = tx.clone();
35        let handle = thread::spawn(move || {
36            match UiRunner::new(
37                jobs,
38                history,
39                current_run_id,
40                job_resources,
41                plan_text,
42                workdir,
43                pipeline_path,
44                tx.clone(),
45                rx,
46                cmd_tx,
47            ) {
48                Ok(runner) => {
49                    if let Err(err) = runner.run() {
50                        eprintln!("ui error: {err:?}");
51                    }
52                }
53                Err(err) => {
54                    eprintln!("ui error: {err:?}");
55                }
56            }
57        });
58        Ok(Self {
59            sender: thread_tx,
60            command_rx: Mutex::new(Some(cmd_rx)),
61            thread: handle,
62        })
63    }
64
65    pub fn bridge(&self) -> UiBridge {
66        UiBridge {
67            sender: self.sender.clone(),
68        }
69    }
70
71    pub fn command_receiver(&self) -> Option<UnboundedReceiver<UiCommand>> {
72        self.command_rx
73            .lock()
74            .ok()
75            .and_then(|mut guard| guard.take())
76    }
77
78    pub fn pipeline_finished(&self) {
79        let _ = self.sender.send(UiEvent::PipelineFinished);
80    }
81
82    pub fn wait_for_exit(self) {
83        let _ = self.thread.join();
84    }
85}
86
87impl UiBridge {
88    pub fn job_started(&self, name: &str) {
89        let _ = self.sender.send(UiEvent::JobStarted {
90            name: name.to_string(),
91        });
92    }
93
94    pub fn job_restarted(&self, name: &str) {
95        let _ = self.sender.send(UiEvent::JobRestarted {
96            name: name.to_string(),
97        });
98    }
99
100    pub fn history_updated(&self, entry: HistoryEntry) {
101        let _ = self.sender.send(UiEvent::HistoryUpdated { entry });
102    }
103
104    pub fn job_log_line(&self, name: &str, line: &str) {
105        let _ = self.sender.send(UiEvent::JobLog {
106            name: name.to_string(),
107            line: line.to_string(),
108        });
109    }
110
111    pub fn job_finished(
112        &self,
113        name: &str,
114        status: UiJobStatus,
115        duration: f32,
116        error: Option<String>,
117    ) {
118        let _ = self.sender.send(UiEvent::JobFinished {
119            name: name.to_string(),
120            status,
121            duration,
122            error,
123        });
124    }
125
126    pub fn job_manual_pending(&self, name: &str) {
127        let _ = self.sender.send(UiEvent::JobManual {
128            name: name.to_string(),
129        });
130    }
131
132    pub fn analysis_started(&self, name: &str, provider: &str) {
133        let _ = self.sender.send(UiEvent::AnalysisStarted {
134            name: name.to_string(),
135            provider: provider.to_string(),
136        });
137    }
138
139    pub fn analysis_chunk(&self, name: &str, delta: &str) {
140        let _ = self.sender.send(UiEvent::AnalysisChunk {
141            name: name.to_string(),
142            delta: delta.to_string(),
143        });
144    }
145
146    pub fn analysis_finished(
147        &self,
148        name: &str,
149        final_text: String,
150        saved_path: Option<PathBuf>,
151        error: Option<String>,
152    ) {
153        let _ = self.sender.send(UiEvent::AnalysisFinished {
154            name: name.to_string(),
155            final_text,
156            saved_path,
157            error,
158        });
159    }
160
161    pub fn ai_prompt_ready(&self, name: &str, prompt: String) {
162        let _ = self.sender.send(UiEvent::AiPromptReady {
163            name: name.to_string(),
164            prompt,
165        });
166    }
167}