use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use dashmap::DashMap;
use worktrunk::git::Repository;
use super::items::{PreviewCache, WorktreeSkimItem};
use super::preview::PreviewMode;
use super::summary;
use crate::commands::list::model::ListItem;
const INITIAL_MODE: PreviewMode = PreviewMode::WorkingTree;
const SECONDARY_MODES: [PreviewMode; 3] = [
PreviewMode::Log,
PreviewMode::BranchDiff,
PreviewMode::UpstreamDiff,
];
struct PendingGuard(Arc<AtomicUsize>);
impl Drop for PendingGuard {
fn drop(&mut self) {
self.0.fetch_sub(1, Ordering::SeqCst);
}
}
pub(super) struct PreviewOrchestrator {
pub(super) cache: PreviewCache,
pending: Arc<AtomicUsize>,
repo: Repository,
}
impl PreviewOrchestrator {
pub(super) fn new(repo: Repository) -> Self {
Self {
cache: Arc::new(DashMap::new()),
pending: Arc::new(AtomicUsize::new(0)),
repo,
}
}
pub(super) fn spawn_preview(
&self,
item: Arc<ListItem>,
mode: PreviewMode,
dims: (usize, usize),
) {
let cache = Arc::clone(&self.cache);
let (w, h) = dims;
let repo = self.repo.clone();
let pending = Arc::clone(&self.pending);
self.spawn_task(move || {
let cache_key = (item.branch_name().to_string(), mode);
if cache.contains_key(&cache_key) {
return;
}
let (value, log_disk_hit) =
WorktreeSkimItem::compute_and_page_preview(&repo, &item, mode, w, h);
cache.insert(cache_key, value);
if log_disk_hit {
pending.fetch_add(1, Ordering::SeqCst);
let guard = PendingGuard(Arc::clone(&pending));
let item = Arc::clone(&item);
let cache = Arc::clone(&cache);
let repo = repo.clone();
rayon::spawn_fifo(move || {
let _g = guard;
let rendered = WorktreeSkimItem::refresh_log_preview(&repo, &item, w, h);
if !rendered.is_empty() {
cache.insert((item.branch_name().to_string(), PreviewMode::Log), rendered);
}
});
}
});
}
pub(super) fn spawn_summary(&self, item: Arc<ListItem>, llm_command: String, repo: Repository) {
let cache = Arc::clone(&self.cache);
self.spawn_task(move || {
summary::generate_and_cache_summary(&item, &llm_command, &cache, &repo);
});
}
pub(super) fn spawn_initial_precompute(
&self,
items: &[Arc<ListItem>],
preview_dims: (usize, usize),
llm_command: Option<&str>,
) {
let Some(first) = items.first() else { return };
self.spawn_preview(Arc::clone(first), INITIAL_MODE, preview_dims);
for mode in SECONDARY_MODES {
self.spawn_preview(Arc::clone(first), mode, preview_dims);
}
if let Some(llm) = llm_command {
self.spawn_summary(Arc::clone(first), llm.to_string(), self.repo.clone());
}
for item in items.iter().skip(1) {
self.spawn_preview(Arc::clone(item), INITIAL_MODE, preview_dims);
}
}
pub(super) fn spawn_deferred_precompute(
&self,
rest: &[Arc<ListItem>],
preview_dims: (usize, usize),
llm_command: Option<&str>,
) {
for mode in SECONDARY_MODES {
for item in rest {
self.spawn_preview(Arc::clone(item), mode, preview_dims);
}
}
if let Some(llm) = llm_command {
for item in rest {
self.spawn_summary(Arc::clone(item), llm.to_string(), self.repo.clone());
}
}
}
pub(super) fn seed_summary_hints(&self, items: &[Arc<ListItem>], hint: &str) {
for item in items {
self.cache.insert(
(item.branch_name().to_string(), PreviewMode::Summary),
hint.to_string(),
);
}
}
fn spawn_task<F: FnOnce() + Send + 'static>(&self, task: F) {
self.pending.fetch_add(1, Ordering::SeqCst);
let guard = PendingGuard(Arc::clone(&self.pending));
let wrapped = move || {
let _g = guard;
task();
};
rayon::spawn(wrapped);
}
pub(super) fn wait_for_idle(&self) {
while self.pending.load(Ordering::SeqCst) > 0 {
std::thread::sleep(Duration::from_millis(10));
}
}
pub(super) fn dump_cache_json(&self) -> String {
let mut entries: Vec<_> = self
.cache
.iter()
.map(|e| {
let (branch, mode) = e.key();
(branch.clone(), *mode as u8, e.value().len())
})
.collect();
entries.sort();
let items: Vec<String> = entries
.iter()
.map(|(branch, mode, bytes)| {
format!(" {{ \"branch\": {branch:?}, \"mode\": {mode}, \"bytes\": {bytes} }}")
})
.collect();
format!("{{\n \"entries\": [\n{}\n ]\n}}", items.join(",\n"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::list::model::{ItemKind, WorktreeData};
use std::fs;
use worktrunk::testing::TestRepo;
fn orch_for(t: &TestRepo) -> PreviewOrchestrator {
PreviewOrchestrator::new(Repository::at(t.path()).unwrap())
}
fn dirty_worktree_item() -> (TestRepo, Arc<ListItem>) {
let t = TestRepo::new();
fs::write(t.path().join("README.md"), "# Project\n").unwrap();
t.repo.run_command(&["add", "README.md"]).unwrap();
t.repo.run_command(&["commit", "-m", "initial"]).unwrap();
fs::write(t.path().join("README.md"), "# Project\nmore\n").unwrap();
let head = t
.repo
.run_command(&["rev-parse", "HEAD"])
.unwrap()
.trim()
.to_string();
let mut item = ListItem::new_branch(head, "main".to_string());
item.kind = ItemKind::Worktree(Box::new(WorktreeData {
path: t.path().to_path_buf(),
..Default::default()
}));
(t, Arc::new(item))
}
#[test]
fn orchestrator_populates_cache_for_real_worktree() {
let (t, item) = dirty_worktree_item();
let orch = orch_for(&t);
orch.spawn_preview(Arc::clone(&item), PreviewMode::WorkingTree, (80, 24));
orch.spawn_preview(Arc::clone(&item), PreviewMode::Log, (80, 24));
orch.wait_for_idle();
let wt_key = ("main".to_string(), PreviewMode::WorkingTree);
let log_key = ("main".to_string(), PreviewMode::Log);
assert!(
orch.cache.contains_key(&wt_key),
"WorkingTree preview not cached"
);
assert!(orch.cache.contains_key(&log_key), "Log preview not cached");
assert!(
!orch.cache.get(&wt_key).unwrap().is_empty(),
"WorkingTree preview was empty"
);
}
#[test]
fn duplicate_spawn_short_circuits() {
let (t, item) = dirty_worktree_item();
let orch = orch_for(&t);
orch.spawn_preview(Arc::clone(&item), PreviewMode::WorkingTree, (80, 24));
orch.wait_for_idle();
let first = orch
.cache
.get(&("main".to_string(), PreviewMode::WorkingTree))
.unwrap()
.value()
.clone();
orch.spawn_preview(Arc::clone(&item), PreviewMode::WorkingTree, (80, 24));
orch.wait_for_idle();
let second = orch
.cache
.get(&("main".to_string(), PreviewMode::WorkingTree))
.unwrap()
.value()
.clone();
assert_eq!(first, second);
}
#[test]
fn spawn_summary_populates_cache() {
let (t, item) = dirty_worktree_item();
let repo = Repository::at(t.path()).unwrap();
let orch = orch_for(&t);
orch.spawn_summary(Arc::clone(&item), "/bin/cat".to_string(), repo);
orch.wait_for_idle();
assert!(
orch.cache
.contains_key(&("main".to_string(), PreviewMode::Summary)),
"Summary entry not cached"
);
}
#[test]
fn log_disk_hit_triggers_background_refresh() {
let (t, item) = dirty_worktree_item();
let repo = Repository::at(t.path()).unwrap();
let stale = super::super::preview_cache::LogCacheEntry {
raw_log: "STALE_MARKER\n".to_string(),
stats: std::collections::HashMap::new(),
};
super::super::preview_cache::write_log(&repo, item.head(), 80, 24, &stale);
let orch = orch_for(&t);
orch.spawn_preview(Arc::clone(&item), PreviewMode::Log, (80, 24));
orch.wait_for_idle();
let disk = super::super::preview_cache::read_log(&repo, item.head(), 80, 24)
.expect("disk cache present after refresh");
assert!(
!disk.raw_log.contains("STALE_MARKER"),
"refresh should overwrite stale disk entry, got raw_log: {:?}",
disk.raw_log
);
let in_memory = orch
.cache
.get(&("main".to_string(), PreviewMode::Log))
.expect("in-memory entry present")
.clone();
assert!(
!in_memory.contains("STALE_MARKER"),
"refresh should overwrite stale in-memory entry, got: {in_memory:?}"
);
}
#[test]
fn non_log_modes_do_not_trigger_log_refresh() {
let (t, item) = dirty_worktree_item();
let repo = Repository::at(t.path()).unwrap();
let stale = super::super::preview_cache::LogCacheEntry {
raw_log: "STALE_MARKER\n".to_string(),
stats: std::collections::HashMap::new(),
};
super::super::preview_cache::write_log(&repo, item.head(), 80, 24, &stale);
let orch = orch_for(&t);
orch.spawn_preview(Arc::clone(&item), PreviewMode::BranchDiff, (80, 24));
orch.wait_for_idle();
let disk = super::super::preview_cache::read_log(&repo, item.head(), 80, 24)
.expect("disk Log cache untouched");
assert_eq!(
disk.raw_log, "STALE_MARKER\n",
"non-Log spawn must not trigger Log refresh"
);
}
#[test]
fn dump_cache_json_format() {
let t = TestRepo::new();
let orch = orch_for(&t);
orch.cache.insert(
("branch-a".to_string(), PreviewMode::WorkingTree),
"x".to_string(),
);
orch.cache
.insert(("branch-b".to_string(), PreviewMode::Log), "xy".to_string());
let json = orch.dump_cache_json();
let parsed: serde_json::Value = serde_json::from_str(&json).expect("valid JSON");
let entries = parsed["entries"].as_array().expect("entries array");
assert_eq!(entries.len(), 2);
}
}