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}