jj workspaces over the network
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(cli): add presence tracking and warnings

+113
+113
crates/tandem-cli/src/presence.rs
··· 1 + use tandem_core::types::{ChangeId, PresenceInfo}; 2 + use tandem_core::sync::ForgeDoc; 3 + use chrono::{Utc, Duration}; 4 + use std::sync::Arc; 5 + use tokio::sync::RwLock; 6 + 7 + /// Presence manager for tracking who's editing what 8 + pub struct PresenceManager { 9 + doc: Arc<RwLock<ForgeDoc>>, 10 + user_id: String, 11 + device: String, 12 + } 13 + 14 + impl PresenceManager { 15 + pub fn new(doc: Arc<RwLock<ForgeDoc>>, user_id: String, device: String) -> Self { 16 + Self { doc, user_id, device } 17 + } 18 + 19 + /// Update our presence (call when user edits a change) 20 + pub async fn update_presence(&self, change_id: &ChangeId) { 21 + let info = PresenceInfo { 22 + user_id: self.user_id.clone(), 23 + change_id: *change_id, 24 + device: self.device.clone(), 25 + timestamp: Utc::now(), 26 + }; 27 + 28 + let doc = self.doc.read().await; 29 + doc.update_presence(&info); 30 + } 31 + 32 + /// Clear our presence (call when leaving a change) 33 + pub async fn clear_presence(&self) { 34 + let doc = self.doc.read().await; 35 + doc.remove_presence(&self.user_id); 36 + } 37 + 38 + /// Get all active presence (excluding stale entries > 5 min) 39 + pub async fn get_active_presence(&self) -> Vec<PresenceInfo> { 40 + let doc = self.doc.read().await; 41 + let all = doc.get_all_presence(); 42 + let cutoff = Utc::now() - Duration::minutes(5); 43 + 44 + all.into_iter() 45 + .filter(|p| p.timestamp > cutoff) 46 + .collect() 47 + } 48 + 49 + /// Check if anyone else is editing this change 50 + pub async fn check_conflict(&self, change_id: &ChangeId) -> Vec<PresenceInfo> { 51 + self.get_active_presence().await 52 + .into_iter() 53 + .filter(|p| &p.change_id == change_id && p.user_id != self.user_id) 54 + .collect() 55 + } 56 + } 57 + 58 + /// Format presence warning message 59 + pub fn format_presence_warning(conflicts: &[PresenceInfo]) -> String { 60 + if conflicts.is_empty() { 61 + return String::new(); 62 + } 63 + 64 + if conflicts.len() == 1 { 65 + let p = &conflicts[0]; 66 + format!( 67 + "⚠ This change is currently being edited by {}@{}", 68 + p.user_id, p.device 69 + ) 70 + } else { 71 + let users: Vec<String> = conflicts 72 + .iter() 73 + .map(|p| format!("{}@{}", p.user_id, p.device)) 74 + .collect(); 75 + format!( 76 + "⚠ This change is currently being edited by: {}", 77 + users.join(", ") 78 + ) 79 + } 80 + } 81 + 82 + /// Format presence info for jj log output 83 + pub fn format_log_presence(presences: &[PresenceInfo], change_id: &ChangeId) -> Option<String> { 84 + let editing: Vec<&PresenceInfo> = presences 85 + .iter() 86 + .filter(|p| &p.change_id == change_id) 87 + .collect(); 88 + 89 + if editing.is_empty() { 90 + return None; 91 + } 92 + 93 + if editing.len() == 1 { 94 + Some(format!("({} editing)", editing[0].user_id)) 95 + } else { 96 + let count = editing.len(); 97 + Some(format!("({} users editing)", count)) 98 + } 99 + } 100 + 101 + /// Prompt user to continue when there's a conflict 102 + pub fn prompt_continue(warning: &str) -> bool { 103 + use std::io::{self, Write}; 104 + 105 + println!("{}", warning); 106 + print!("Continue anyway? [y/N] "); 107 + io::stdout().flush().unwrap(); 108 + 109 + let mut input = String::new(); 110 + io::stdin().read_line(&mut input).unwrap(); 111 + 112 + matches!(input.trim().to_lowercase().as_str(), "y" | "yes") 113 + }