use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Mutex;
use uuid::Uuid;
use super::types::{MessageEnqueueCallback, QueuedUserMessage};
#[derive(Debug, Clone)]
pub struct CmdResult {
pub success: bool,
pub code: i32,
pub output: String,
}
pub struct BackgroundTaskManager {
enqueue: MessageEnqueueCallback,
running: Mutex<HashMap<Uuid, usize>>,
}
impl BackgroundTaskManager {
pub fn new(enqueue: MessageEnqueueCallback) -> Self {
Self {
enqueue,
running: Mutex::new(HashMap::new()),
}
}
pub fn running_for(&self, session_id: Uuid) -> usize {
self.running
.lock()
.map(|m| m.get(&session_id).copied().unwrap_or(0))
.unwrap_or(0)
}
fn mark_started(&self, session_id: Uuid) {
if let Ok(mut m) = self.running.lock() {
*m.entry(session_id).or_insert(0) += 1;
}
}
fn mark_finished(&self, session_id: Uuid) {
if let Ok(mut m) = self.running.lock()
&& let Some(n) = m.get_mut(&session_id)
{
*n = n.saturating_sub(1);
if *n == 0 {
m.remove(&session_id);
}
}
}
pub fn spawn_command(
self: std::sync::Arc<Self>,
session_id: Uuid,
cwd: PathBuf,
label: String,
command: String,
) {
self.mark_started(session_id);
let this = std::sync::Arc::clone(&self);
tokio::spawn(async move {
let result = run_detached(&command, &cwd).await;
tracing::info!(
target: "background_task",
"Background task '{label}' for session {session_id} finished (success={})",
result.success
);
let msg = completion_message(&label, &command, &result);
(this.enqueue)(session_id, msg);
this.mark_finished(session_id);
});
}
}
async fn run_detached(command: &str, cwd: &std::path::Path) -> CmdResult {
use tokio::process::Command;
let output = Command::new("sh")
.arg("-c")
.arg(command)
.current_dir(cwd)
.output()
.await;
match output {
Ok(out) => {
let mut combined = String::from_utf8_lossy(&out.stdout).into_owned();
let err = String::from_utf8_lossy(&out.stderr);
if !err.trim().is_empty() {
if !combined.is_empty() {
combined.push('\n');
}
combined.push_str(&err);
}
CmdResult {
success: out.status.success(),
code: out.status.code().unwrap_or(-1),
output: combined,
}
}
Err(e) => CmdResult {
success: false,
code: -1,
output: format!("failed to launch: {e}"),
},
}
}
const KNOWN_LONG_MARKERS: &[&str] = &[
"cargo test",
"cargo build",
"cargo run",
"cargo clippy",
"npm test",
"npm run build",
"pnpm test",
"pnpm build",
"yarn test",
"yarn build",
"npx remotion render",
"remotion render",
"gh run watch",
"gh pr checks --watch",
];
pub(crate) fn is_known_long(command: &str) -> bool {
let lower = command.to_lowercase();
KNOWN_LONG_MARKERS.iter().any(|m| lower.contains(m))
}
pub(crate) fn short_label(command: &str) -> String {
let trimmed = command.trim();
let after_cd = trimmed.rsplit("&&").next().unwrap_or(trimmed).trim();
let label: String = after_cd.chars().take(60).collect();
if after_cd.chars().count() > 60 {
format!("{label}…")
} else {
label
}
}
pub(crate) fn tail_lines(text: &str, n: usize) -> String {
let lines: Vec<&str> = text.lines().collect();
let start = lines.len().saturating_sub(n);
lines[start..].join("\n")
}
pub(crate) fn completion_message(
label: &str,
command: &str,
result: &CmdResult,
) -> QueuedUserMessage {
let status = if result.success {
"exit 0 (success)".to_string()
} else {
format!("exit {} (failure)", result.code)
};
let tail = tail_lines(&result.output, 50);
let context = format!(
"[System: the background task you started has finished.\n\
Task: {label}\n\
Command: {command}\n\
Status: {status}\n\
Output (last 50 lines):\n{tail}\n\n\
Report the result to the user and continue anything that was waiting on it. \
Do not re-run the command — this IS its result.]"
);
let display = format!(
"🔧 background task {}: {label}",
if result.success { "finished" } else { "failed" }
);
QueuedUserMessage::system(context, display)
}