use std::collections::HashMap;
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use crate::tmux::{self, DiffStats, SessionStatus, TmuxSession};
#[derive(Clone)]
pub enum Selection {
None,
Task {
project_name: String,
project_path: String,
branch: String,
},
Session {
name: String,
preview_mode: PreviewMode,
},
}
#[derive(Clone, Copy, PartialEq)]
pub enum PreviewMode {
Output,
Diff,
Context,
Terminal(usize), }
#[derive(Clone)]
pub struct TaskInfo {
pub project_name: String,
pub project_path: String,
pub branch: String,
}
pub struct WorkerHints {
pub selection: Selection,
pub tasks: Vec<TaskInfo>,
pub project_paths: Vec<(String, String)>,
}
pub struct WorkerUpdate {
pub sessions: Vec<TmuxSession>,
pub statuses: HashMap<String, SessionStatus>,
pub diff_stats: HashMap<String, DiffStats>,
pub preview_content: Option<String>,
pub task_diff: Option<DiffStats>,
pub task_context_content: Option<String>,
pub task_diff_stats: HashMap<String, DiffStats>,
pub merged_sessions: HashMap<String, bool>,
pub terminal_counts: HashMap<String, usize>,
pub pr_urls: HashMap<String, String>,
pub project_branches: HashMap<String, String>,
}
pub struct Worker {
pub hints: Arc<Mutex<WorkerHints>>,
pub receiver: mpsc::Receiver<WorkerUpdate>,
}
impl Worker {
pub fn spawn() -> Self {
let hints = Arc::new(Mutex::new(WorkerHints {
selection: Selection::None,
tasks: Vec::new(),
project_paths: Vec::new(),
}));
let (tx, rx) = mpsc::channel();
let hints_clone = hints.clone();
thread::spawn(move || worker_loop(hints_clone, tx));
Worker {
hints,
receiver: rx,
}
}
}
fn worker_loop(hints: Arc<Mutex<WorkerHints>>, tx: mpsc::Sender<WorkerUpdate>) {
let mut content_hashes: HashMap<String, u64> = HashMap::new();
let mut stable_ticks: HashMap<String, u32> = HashMap::new();
let mut diff_stats: HashMap<String, DiffStats> = HashMap::new();
let mut merged_sessions: HashMap<String, bool> = HashMap::new();
let mut terminal_counts: HashMap<String, usize> = HashMap::new();
let mut pr_urls: HashMap<String, String> = HashMap::new();
let mut tick: u64 = 0;
loop {
let sessions = tmux::list_sessions().unwrap_or_default();
let mut statuses = HashMap::new();
const STABLE_THRESHOLD: u32 = 3;
for session in &sessions {
let probe = tmux::probe_session(&session.name);
let status = match probe {
None => {
content_hashes.remove(&session.name);
stable_ticks.remove(&session.name);
SessionStatus::Finished
}
Some(probe) if !probe.claude_alive => {
content_hashes.remove(&session.name);
stable_ticks.remove(&session.name);
SessionStatus::Finished
}
Some(probe) => {
let prev_hash = content_hashes.get(&session.name).copied();
let content_changed = prev_hash.is_some_and(|h| h != probe.content_hash);
content_hashes.insert(session.name.clone(), probe.content_hash);
if content_changed {
stable_ticks.insert(session.name.clone(), 0);
SessionStatus::Running
} else {
let ticks = stable_ticks.entry(session.name.clone()).or_insert(0);
*ticks = ticks.saturating_add(1);
if *ticks < STABLE_THRESHOLD {
SessionStatus::Running
} else if probe.has_permission_prompt {
SessionStatus::WaitingForPermission
} else {
SessionStatus::WaitingForInput
}
}
}
};
statuses.insert(session.name.clone(), status);
}
if tick % 4 == 0 {
let session_names: Vec<String> = sessions.iter().map(|s| s.name.clone()).collect();
diff_stats.retain(|k, _| session_names.contains(k));
merged_sessions.retain(|k, _| session_names.contains(k));
terminal_counts.retain(|k, _| session_names.contains(k));
for session in &sessions {
if let Some(stats) = tmux::get_diff_stats(&session.name) {
diff_stats.insert(session.name.clone(), stats);
}
if let Some(merged) = tmux::is_session_merged(&session.name) {
merged_sessions.insert(session.name.clone(), merged);
}
let count = tmux::count_terminal_windows(&session.name);
terminal_counts.insert(session.name.clone(), count);
}
}
let (selection, tasks, project_paths) = {
let h = hints.lock().unwrap();
(
h.selection.clone(),
h.tasks.clone(),
h.project_paths.clone(),
)
};
let mut task_diff_stats: HashMap<String, DiffStats> = HashMap::new();
if tick % 4 == 0 {
for task in &tasks {
if let Some(stats) = tmux::get_branch_diff(&task.project_path, &task.branch) {
task_diff_stats.insert(task.branch.clone(), stats);
}
}
}
if tick % 20 == 0 {
for task in &tasks {
if !pr_urls.contains_key(&task.branch) {
if let Some(url) = tmux::get_pr_url(&task.project_path, &task.branch) {
let pr_path = crate::config::pr_url_path(&task.project_name, &task.branch);
if let Some(parent) = pr_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(&pr_path, &url);
pr_urls.insert(task.branch.clone(), url);
}
}
}
}
let mut project_branches: HashMap<String, String> = HashMap::new();
if tick % 4 == 0 {
for (name, path) in &project_paths {
if let Some(branch) = get_current_branch(path) {
project_branches.insert(name.clone(), branch);
}
}
}
let (preview_content, task_diff, task_context_content) = match &selection {
Selection::None => (None, None, None),
Selection::Task {
project_name,
project_path,
branch,
} => {
let diff = if tick % 4 == 0 {
tmux::get_branch_diff(project_path, branch)
} else {
None
};
let context = {
let ctx_path = crate::config::task_context_path(project_name, branch);
std::fs::read_to_string(&ctx_path).ok()
};
(None, diff, context)
}
Selection::Session { name, preview_mode } => {
let content = match preview_mode {
PreviewMode::Output => tmux::capture_pane(&format!("{name}:0")),
PreviewMode::Diff => diff_stats.get(name).map(|s| s.diff_output.clone()),
PreviewMode::Context => None,
PreviewMode::Terminal(idx) => {
let target = format!("{name}:{}", idx + 1);
tmux::capture_pane(&target)
}
};
(content, None, None)
}
};
let update = WorkerUpdate {
sessions,
statuses,
diff_stats: diff_stats.clone(),
merged_sessions: merged_sessions.clone(),
preview_content,
task_diff,
task_context_content,
task_diff_stats,
terminal_counts: terminal_counts.clone(),
pr_urls: pr_urls.clone(),
project_branches,
};
if tx.send(update).is_err() {
break;
}
tick += 1;
thread::sleep(Duration::from_millis(500));
}
}
fn get_current_branch(project_path: &str) -> Option<String> {
let output = std::process::Command::new("git")
.args(["-C", project_path, "rev-parse", "--abbrev-ref", "HEAD"])
.output()
.ok()?;
if output.status.success() {
Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
None
}
}