1use std::collections::HashMap;
5use std::sync::Arc;
6use tokio::sync::Mutex;
7
8pub const POLL_INTERVAL_MS: u64 = 1000;
10
11pub const STOPPED_DISPLAY_MS: u64 = 3_000;
13
14pub const PANEL_GRACE_MS: u64 = 30_000;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19#[allow(dead_code)]
20pub enum TaskType {
21 LocalBash,
22 LocalAgent,
23 RemoteAgent,
24 InProcessTeammate,
25 LocalWorkflow,
26 MonitorMcp,
27 Dream,
28}
29
30impl TaskType {
31 pub fn as_str(&self) -> &'static str {
32 match self {
33 TaskType::LocalBash => "local_bash",
34 TaskType::LocalAgent => "local_agent",
35 TaskType::RemoteAgent => "remote_agent",
36 TaskType::InProcessTeammate => "in_process_teammate",
37 TaskType::LocalWorkflow => "local_workflow",
38 TaskType::MonitorMcp => "monitor_mcp",
39 TaskType::Dream => "dream",
40 }
41 }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46#[allow(dead_code)]
47pub enum TaskStatus {
48 Pending,
49 Running,
50 Completed,
51 Failed,
52 Killed,
53}
54
55impl TaskStatus {
56 pub fn as_str(&self) -> &'static str {
57 match self {
58 TaskStatus::Pending => "pending",
59 TaskStatus::Running => "running",
60 TaskStatus::Completed => "completed",
61 TaskStatus::Failed => "failed",
62 TaskStatus::Killed => "killed",
63 }
64 }
65}
66
67pub fn is_terminal_task_status(status: &TaskStatus) -> bool {
69 matches!(
70 status,
71 TaskStatus::Completed | TaskStatus::Failed | TaskStatus::Killed
72 )
73}
74
75#[derive(Debug, Clone)]
77#[allow(dead_code)]
78pub struct TaskStateBase {
79 pub id: String,
80 pub task_type: TaskType,
81 pub status: TaskStatus,
82 pub description: String,
83 pub tool_use_id: Option<String>,
84 pub start_time: u64,
85 pub end_time: Option<u64>,
86 pub total_paused_ms: Option<u64>,
87 pub output_file: String,
88 pub output_offset: u64,
89 pub notified: bool,
90 pub retain: Option<bool>,
92 pub evict_after: Option<u64>,
93 pub messages: Option<String>,
94 pub disk_loaded: Option<bool>,
95 pub pending_messages: Option<Vec<String>>,
96}
97
98#[derive(Debug, Clone)]
100pub struct TaskAttachment {
101 pub task_id: String,
102 pub tool_use_id: Option<String>,
103 pub task_type: TaskType,
104 pub status: TaskStatus,
105 pub description: String,
106 pub delta_summary: Option<String>, }
108
109pub type AppState = HashMap<String, TaskStateBase>;
111
112pub type SetAppState = Arc<dyn Fn(Arc<Mutex<AppState>>) -> Arc<Mutex<AppState>> + Send + Sync>;
114
115pub fn update_task_state<T: TaskStateTrait>(
119 task_id: &str,
120 set_app_state: &SetAppState,
121 updater: impl FnOnce(&T) -> T,
122) {
123 let app_state = Arc::new(Mutex::new(HashMap::new()));
124 let new_state = set_app_state(app_state.clone());
125
126 }
129
130pub trait TaskStateTrait {
132 fn as_any(&self) -> &dyn std::any::Any;
133 fn clone_box(&self) -> Box<dyn TaskStateTrait>;
134}
135
136impl<T: Clone + 'static> TaskStateTrait for T {
137 fn as_any(&self) -> &dyn std::any::Any {
138 self
139 }
140
141 fn clone_box(&self) -> Box<dyn TaskStateTrait> {
142 Box::new(self.clone())
143 }
144}
145
146pub fn register_task(task: TaskStateBase, set_app_state: &SetAppState) -> bool {
148 let mut is_replacement = false;
149
150 let _ = (task, set_app_state);
154
155 is_replacement
159}
160
161pub fn evict_terminal_task(task_id: &str, set_app_state: &SetAppState) {
164 let _ = (task_id, set_app_state);
165 }
171
172pub fn get_running_tasks(state: &AppState) -> Vec<&TaskStateBase> {
174 state
175 .values()
176 .filter(|task| task.status == TaskStatus::Running)
177 .collect()
178}
179
180pub async fn generate_task_attachments(
183 state: Arc<Mutex<AppState>>,
184) -> (
185 Vec<TaskAttachment>,
186 HashMap<String, u64>, Vec<String>, ) {
189 let mut attachments = Vec::new();
190 let mut updated_task_offsets: HashMap<String, u64> = HashMap::new();
191 let mut evicted_task_ids: Vec<String> = Vec::new();
192
193 let tasks = state.lock().await;
194
195 for (task_id, task_state) in tasks.iter() {
196 if task_state.notified {
197 match task_state.status {
198 TaskStatus::Completed | TaskStatus::Failed | TaskStatus::Killed => {
199 evicted_task_ids.push(task_id.clone());
201 continue;
202 }
203 TaskStatus::Pending => {
204 continue;
206 }
207 TaskStatus::Running => {
208 }
210 }
211 }
212
213 if task_state.status == TaskStatus::Running {
214 updated_task_offsets.insert(task_id.clone(), task_state.output_offset);
218 }
219 }
220
221 (attachments, updated_task_offsets, evicted_task_ids)
222}
223
224pub fn apply_task_offsets_and_evictions(
227 set_app_state: &SetAppState,
228 updated_task_offsets: HashMap<String, u64>,
229 evicted_task_ids: Vec<String>,
230) {
231 if updated_task_offsets.is_empty() && evicted_task_ids.is_empty() {
232 return;
233 }
234
235 let _ = (set_app_state, updated_task_offsets, evicted_task_ids);
236 }
238
239pub async fn poll_tasks(
242 get_app_state: impl Fn() -> Arc<Mutex<AppState>>,
243 set_app_state: &SetAppState,
244) {
245 let state = get_app_state();
246 let (attachments, updated_task_offsets, evicted_task_ids) =
247 generate_task_attachments(state).await;
248
249 apply_task_offsets_and_evictions(set_app_state, updated_task_offsets, evicted_task_ids);
250
251 for attachment in attachments {
253 enqueue_task_notification(attachment);
254 }
255}
256
257fn enqueue_task_notification(attachment: TaskAttachment) {
259 let status_text = get_status_text(&attachment.status);
260
261 let _ = (attachment, status_text);
264 }
266
267fn get_status_text(status: &TaskStatus) -> &'static str {
269 match status {
270 TaskStatus::Completed => "completed successfully",
271 TaskStatus::Failed => "failed",
272 TaskStatus::Killed => "was stopped",
273 TaskStatus::Running => "is running",
274 TaskStatus::Pending => "is pending",
275 }
276}
277
278pub const TASK_NOTIFICATION_TAG: &str = "task-notification";
280pub const TASK_ID_TAG: &str = "task-id";
281pub const TOOL_USE_ID_TAG: &str = "tool-use-id";
282pub const TASK_TYPE_TAG: &str = "task-type";
283pub const OUTPUT_FILE_TAG: &str = "output-file";
284pub const STATUS_TAG: &str = "status";
285pub const SUMMARY_TAG: &str = "summary";
286
287pub fn format_task_notification(
289 task_id: &str,
290 tool_use_id: Option<&str>,
291 task_type: &TaskType,
292 output_file: &str,
293 status: &TaskStatus,
294 description: &str,
295) -> String {
296 let tool_use_id_line = tool_use_id
297 .map(|id| format!("\n<{}>{}</{}>", TOOL_USE_ID_TAG, id, TOOL_USE_ID_TAG))
298 .unwrap_or_default();
299
300 let status_text = get_status_text(status);
301
302 format!(
303 "<{}>\
304<{}>{}</{}>{}\
305<{}>{}</{}>\
306<{}>{}</{}>\
307<{}>{}</{}>\
308<{}>Task \"{}\" {}</{}>\
309</{}>",
310 TASK_NOTIFICATION_TAG,
311 TASK_ID_TAG,
312 task_id,
313 TASK_ID_TAG,
314 tool_use_id_line,
315 TASK_TYPE_TAG,
316 task_type.as_str(),
317 TASK_TYPE_TAG,
318 OUTPUT_FILE_TAG,
319 output_file,
320 OUTPUT_FILE_TAG,
321 STATUS_TAG,
322 status.as_str(),
323 STATUS_TAG,
324 SUMMARY_TAG,
325 description,
326 status_text,
327 SUMMARY_TAG,
328 TASK_NOTIFICATION_TAG
329 )
330}
331
332#[derive(Debug, Clone)]
334pub struct TaskOutput {
335 pub task_id: String,
336 pub content: String,
337 pub timestamp: i64,
338}
339
340pub const MAX_TASK_OUTPUT_BYTES: usize = 100_000;
342
343pub const MAX_TASK_OUTPUT_BYTES_DISPLAY: &str = "100KB";
345
346#[allow(dead_code)]
348pub fn init_task_output(_task_id: &str) -> std::path::PathBuf {
349 std::env::temp_dir().join("task_output.txt")
351}
352
353#[allow(dead_code)]
355pub fn init_task_output_as_symlink(
356 _task_id: &str,
357 _target: &std::path::Path,
358) -> std::io::Result<()> {
359 Ok(())
361}
362
363#[allow(dead_code)]
365pub fn get_task_output_path(_task_id: &str) -> std::path::PathBuf {
366 std::env::temp_dir().join("task_output.txt")
367}
368
369#[allow(dead_code)]
371pub fn get_task_output_size(_task_id: &str) -> usize {
372 0
373}
374
375#[allow(dead_code)]
377pub fn get_task_output(_task_id: &str) -> Option<TaskOutput> {
378 None
379}
380
381#[allow(dead_code)]
383pub fn get_task_output_delta(_task_id: &str, _from_byte: usize) -> Option<String> {
384 None
385}
386
387#[allow(dead_code)]
389pub fn append_task_output(_task_id: &str, _content: &str) -> std::io::Result<usize> {
390 Ok(0)
391}
392
393#[allow(dead_code)]
395pub fn cleanup_task_output(_task_id: &str) -> std::io::Result<()> {
396 Ok(())
397}
398
399#[allow(dead_code)]
401pub fn evict_task_output(_task_id: &str) -> std::io::Result<()> {
402 Ok(())
403}
404
405#[allow(dead_code)]
407pub fn flush_task_output(_task_id: &str) -> std::io::Result<()> {
408 Ok(())
409}