use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
pub const POLL_INTERVAL_MS: u64 = 1000;
pub const STOPPED_DISPLAY_MS: u64 = 3_000;
pub const PANEL_GRACE_MS: u64 = 30_000;
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(dead_code)]
pub enum TaskType {
LocalBash,
LocalAgent,
RemoteAgent,
InProcessTeammate,
LocalWorkflow,
MonitorMcp,
Dream,
}
impl TaskType {
pub fn as_str(&self) -> &'static str {
match self {
TaskType::LocalBash => "local_bash",
TaskType::LocalAgent => "local_agent",
TaskType::RemoteAgent => "remote_agent",
TaskType::InProcessTeammate => "in_process_teammate",
TaskType::LocalWorkflow => "local_workflow",
TaskType::MonitorMcp => "monitor_mcp",
TaskType::Dream => "dream",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(dead_code)]
pub enum TaskStatus {
Pending,
Running,
Completed,
Failed,
Killed,
}
impl TaskStatus {
pub fn as_str(&self) -> &'static str {
match self {
TaskStatus::Pending => "pending",
TaskStatus::Running => "running",
TaskStatus::Completed => "completed",
TaskStatus::Failed => "failed",
TaskStatus::Killed => "killed",
}
}
}
pub fn is_terminal_task_status(status: &TaskStatus) -> bool {
matches!(
status,
TaskStatus::Completed | TaskStatus::Failed | TaskStatus::Killed
)
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct TaskStateBase {
pub id: String,
pub task_type: TaskType,
pub status: TaskStatus,
pub description: String,
pub tool_use_id: Option<String>,
pub start_time: u64,
pub end_time: Option<u64>,
pub total_paused_ms: Option<u64>,
pub output_file: String,
pub output_offset: u64,
pub notified: bool,
pub retain: Option<bool>,
pub evict_after: Option<u64>,
pub messages: Option<String>,
pub disk_loaded: Option<bool>,
pub pending_messages: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
pub struct TaskAttachment {
pub task_id: String,
pub tool_use_id: Option<String>,
pub task_type: TaskType,
pub status: TaskStatus,
pub description: String,
pub delta_summary: Option<String>, }
pub type AppState = HashMap<String, TaskStateBase>;
pub type SetAppState = Arc<dyn Fn(Arc<Mutex<AppState>>) -> Arc<Mutex<AppState>> + Send + Sync>;
pub fn update_task_state<T: TaskStateTrait>(
task_id: &str,
set_app_state: &SetAppState,
updater: impl FnOnce(&T) -> T,
) {
let app_state = Arc::new(Mutex::new(HashMap::new()));
let new_state = set_app_state(app_state.clone());
}
pub trait TaskStateTrait {
fn as_any(&self) -> &dyn std::any::Any;
fn clone_box(&self) -> Box<dyn TaskStateTrait>;
}
impl<T: Clone + 'static> TaskStateTrait for T {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn clone_box(&self) -> Box<dyn TaskStateTrait> {
Box::new(self.clone())
}
}
pub fn register_task(task: TaskStateBase, set_app_state: &SetAppState) -> bool {
let mut is_replacement = false;
let task_for_emit = task.clone();
let _app_state = Arc::new(Mutex::new(HashMap::new()));
let existing_state = set_app_state(_app_state.clone());
let mut state = existing_state.blocking_lock();
is_replacement = state.contains_key(&task_for_emit.id);
if !is_replacement {
state.insert(task_for_emit.id.clone(), task_for_emit.clone());
}
drop(state);
if is_replacement {
return false;
}
crate::utils::sdk_event_queue::emit_task_started(
&task_for_emit.id,
task_for_emit.tool_use_id.clone(),
&task_for_emit.description,
Some(task_for_emit.task_type.as_str().to_string()),
None, None, );
is_replacement
}
pub fn evict_terminal_task(task_id: &str, set_app_state: &SetAppState) {
let _ = (task_id, set_app_state);
}
pub fn get_running_tasks(state: &AppState) -> Vec<&TaskStateBase> {
state
.values()
.filter(|task| task.status == TaskStatus::Running)
.collect()
}
pub async fn generate_task_attachments(
state: Arc<Mutex<AppState>>,
) -> (
Vec<TaskAttachment>,
HashMap<String, u64>, // updated task offsets
Vec<String>, // evicted task ids
) {
let mut attachments = Vec::new();
let mut updated_task_offsets: HashMap<String, u64> = HashMap::new();
let mut evicted_task_ids: Vec<String> = Vec::new();
let tasks = state.lock().await;
for (task_id, task_state) in tasks.iter() {
if task_state.notified {
match task_state.status {
TaskStatus::Completed | TaskStatus::Failed | TaskStatus::Killed => {
evicted_task_ids.push(task_id.clone());
continue;
}
TaskStatus::Pending => {
continue;
}
TaskStatus::Running => {
}
}
}
if task_state.status == TaskStatus::Running {
updated_task_offsets.insert(task_id.clone(), task_state.output_offset);
}
}
(attachments, updated_task_offsets, evicted_task_ids)
}
pub fn apply_task_offsets_and_evictions(
set_app_state: &SetAppState,
updated_task_offsets: HashMap<String, u64>,
evicted_task_ids: Vec<String>,
) {
if updated_task_offsets.is_empty() && evicted_task_ids.is_empty() {
return;
}
let _ = (set_app_state, updated_task_offsets, evicted_task_ids);
}
pub async fn poll_tasks(
get_app_state: impl Fn() -> Arc<Mutex<AppState>>,
set_app_state: &SetAppState,
) {
let state = get_app_state();
let (attachments, updated_task_offsets, evicted_task_ids) =
generate_task_attachments(state).await;
apply_task_offsets_and_evictions(set_app_state, updated_task_offsets, evicted_task_ids);
for attachment in attachments {
enqueue_task_notification(attachment);
}
}
fn enqueue_task_notification(attachment: TaskAttachment) {
let status_text = get_status_text(&attachment.status);
let _ = (attachment, status_text);
}
fn get_status_text(status: &TaskStatus) -> &'static str {
match status {
TaskStatus::Completed => "completed successfully",
TaskStatus::Failed => "failed",
TaskStatus::Killed => "was stopped",
TaskStatus::Running => "is running",
TaskStatus::Pending => "is pending",
}
}
pub const TASK_NOTIFICATION_TAG: &str = "task-notification";
pub const TASK_ID_TAG: &str = "task-id";
pub const TOOL_USE_ID_TAG: &str = "tool-use-id";
pub const TASK_TYPE_TAG: &str = "task-type";
pub const OUTPUT_FILE_TAG: &str = "output-file";
pub const STATUS_TAG: &str = "status";
pub const SUMMARY_TAG: &str = "summary";
pub fn format_task_notification(
task_id: &str,
tool_use_id: Option<&str>,
task_type: &TaskType,
output_file: &str,
status: &TaskStatus,
description: &str,
) -> String {
let tool_use_id_line = tool_use_id
.map(|id| format!("\n<{}>{}</{}>", TOOL_USE_ID_TAG, id, TOOL_USE_ID_TAG))
.unwrap_or_default();
let status_text = get_status_text(status);
format!(
"<{}>\
<{}>{}</{}>{}\
<{}>{}</{}>\
<{}>{}</{}>\
<{}>{}</{}>\
<{}>Task \"{}\" {}</{}>\
</{}>",
TASK_NOTIFICATION_TAG,
TASK_ID_TAG,
task_id,
TASK_ID_TAG,
tool_use_id_line,
TASK_TYPE_TAG,
task_type.as_str(),
TASK_TYPE_TAG,
OUTPUT_FILE_TAG,
output_file,
OUTPUT_FILE_TAG,
STATUS_TAG,
status.as_str(),
STATUS_TAG,
SUMMARY_TAG,
description,
status_text,
SUMMARY_TAG,
TASK_NOTIFICATION_TAG
)
}
#[derive(Debug, Clone)]
pub struct TaskOutput {
pub task_id: String,
pub content: String,
pub timestamp: i64,
}
pub const MAX_TASK_OUTPUT_BYTES: usize = 100_000;
pub const MAX_TASK_OUTPUT_BYTES_DISPLAY: &str = "100KB";
#[allow(dead_code)]
pub fn init_task_output(_task_id: &str) -> std::path::PathBuf {
std::env::temp_dir().join("task_output.txt")
}
#[allow(dead_code)]
pub fn init_task_output_as_symlink(
_task_id: &str,
_target: &std::path::Path,
) -> std::io::Result<()> {
Ok(())
}
#[allow(dead_code)]
pub fn get_task_output_path(_task_id: &str) -> std::path::PathBuf {
std::env::temp_dir().join("task_output.txt")
}
#[allow(dead_code)]
pub fn get_task_output_size(_task_id: &str) -> usize {
0
}
#[allow(dead_code)]
pub fn get_task_output(_task_id: &str) -> Option<TaskOutput> {
None
}
#[allow(dead_code)]
pub fn get_task_output_delta(_task_id: &str, _from_byte: usize) -> Option<String> {
None
}
#[allow(dead_code)]
pub fn append_task_output(_task_id: &str, _content: &str) -> std::io::Result<usize> {
Ok(0)
}
#[allow(dead_code)]
pub fn cleanup_task_output(_task_id: &str) -> std::io::Result<()> {
Ok(())
}
#[allow(dead_code)]
pub fn evict_task_output(_task_id: &str) -> std::io::Result<()> {
Ok(())
}
#[allow(dead_code)]
pub fn flush_task_output(_task_id: &str) -> std::io::Result<()> {
Ok(())
}