thulp_workspace/
lib.rs

1//! # thulp-workspace
2//!
3//! Workspace and session management for thulp.
4//!
5//! This crate provides functionality for managing agent workspaces,
6//! including context, state, and session persistence.
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use std::collections::HashMap;
11use std::path::PathBuf;
12
13use std::fs;
14use std::path::Path;
15
16/// Result type for workspace operations
17pub type Result<T> = std::result::Result<T, WorkspaceError>;
18
19/// Errors that can occur in workspace operations
20#[derive(Debug, thiserror::Error)]
21pub enum WorkspaceError {
22    #[error("IO error: {0}")]
23    Io(#[from] std::io::Error),
24
25    #[error("Serialization error: {0}")]
26    Serialization(String),
27
28    #[error("Workspace not found: {0}")]
29    NotFound(String),
30}
31
32/// A workspace for an agent session
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct Workspace {
35    /// Workspace ID
36    pub id: String,
37
38    /// Workspace name
39    pub name: String,
40
41    /// Root directory path
42    pub root: PathBuf,
43
44    /// Workspace metadata
45    #[serde(default)]
46    pub metadata: HashMap<String, String>,
47
48    /// Context data
49    #[serde(default)]
50    pub context: HashMap<String, Value>,
51}
52
53impl Workspace {
54    /// Create a new workspace
55    pub fn new(id: impl Into<String>, name: impl Into<String>, root: PathBuf) -> Self {
56        Self {
57            id: id.into(),
58            name: name.into(),
59            root,
60            metadata: HashMap::new(),
61            context: HashMap::new(),
62        }
63    }
64
65    /// Set metadata
66    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
67        self.metadata.insert(key.into(), value.into());
68        self
69    }
70
71    /// Set context data
72    pub fn with_context(mut self, key: impl Into<String>, value: Value) -> Self {
73        self.context.insert(key.into(), value);
74        self
75    }
76
77    /// Get context value
78    pub fn get_context(&self, key: &str) -> Option<&Value> {
79        self.context.get(key)
80    }
81
82    /// Get metadata value
83    pub fn get_metadata(&self, key: &str) -> Option<&String> {
84        self.metadata.get(key)
85    }
86}
87
88impl Workspace {
89    /// Save the workspace to a JSON file
90    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
91        let json = serde_json::to_string_pretty(self)
92            .map_err(|e| WorkspaceError::Serialization(e.to_string()))?;
93        fs::write(path, json).map_err(WorkspaceError::Io)?;
94        Ok(())
95    }
96
97    /// Load a workspace from a JSON file
98    pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
99        let json = fs::read_to_string(path).map_err(WorkspaceError::Io)?;
100        let workspace = serde_json::from_str(&json)
101            .map_err(|e| WorkspaceError::Serialization(e.to_string()))?;
102        Ok(workspace)
103    }
104}
105
106/// Manager for multiple workspaces
107#[derive(Debug, Default)]
108pub struct WorkspaceManager {
109    workspaces: HashMap<String, Workspace>,
110    active_workspace: Option<String>,
111}
112
113impl WorkspaceManager {
114    /// Create a new workspace manager
115    pub fn new() -> Self {
116        Self::default()
117    }
118
119    /// Create and register a new workspace
120    pub fn create(&mut self, workspace: Workspace) {
121        self.workspaces.insert(workspace.id.clone(), workspace);
122    }
123
124    /// Get a workspace by ID
125    pub fn get(&self, id: &str) -> Option<&Workspace> {
126        self.workspaces.get(id)
127    }
128
129    /// Get a mutable workspace by ID
130    pub fn get_mut(&mut self, id: &str) -> Option<&mut Workspace> {
131        self.workspaces.get_mut(id)
132    }
133
134    /// Set the active workspace
135    pub fn set_active(&mut self, id: &str) -> Result<()> {
136        if !self.workspaces.contains_key(id) {
137            return Err(WorkspaceError::NotFound(id.to_string()));
138        }
139        self.active_workspace = Some(id.to_string());
140        Ok(())
141    }
142
143    /// Get the active workspace
144    pub fn get_active(&self) -> Option<&Workspace> {
145        self.active_workspace
146            .as_ref()
147            .and_then(|id| self.workspaces.get(id))
148    }
149
150    /// Get the active workspace mutably
151    pub fn get_active_mut(&mut self) -> Option<&mut Workspace> {
152        self.active_workspace
153            .as_ref()
154            .and_then(|id| self.workspaces.get_mut(id))
155    }
156
157    /// List all workspace IDs
158    pub fn list(&self) -> Vec<String> {
159        self.workspaces.keys().cloned().collect()
160    }
161
162    /// Remove a workspace
163    pub fn remove(&mut self, id: &str) -> Option<Workspace> {
164        if self.active_workspace.as_deref() == Some(id) {
165            self.active_workspace = None;
166        }
167        self.workspaces.remove(id)
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn test_workspace_creation() {
177        let workspace = Workspace::new("test", "Test Workspace", PathBuf::from("/tmp/test"));
178        assert_eq!(workspace.id, "test");
179        assert_eq!(workspace.name, "Test Workspace");
180    }
181
182    #[test]
183    fn test_workspace_builder() {
184        let workspace = Workspace::new("test", "Test", PathBuf::from("/tmp"))
185            .with_metadata("version", "1.0")
186            .with_context("key", serde_json::json!({"value": 42}));
187
188        assert_eq!(workspace.get_metadata("version"), Some(&"1.0".to_string()));
189        assert!(workspace.get_context("key").is_some());
190    }
191
192    #[test]
193    fn test_workspace_manager() {
194        let mut manager = WorkspaceManager::new();
195
196        let workspace = Workspace::new("test", "Test", PathBuf::from("/tmp"));
197        manager.create(workspace);
198
199        assert!(manager.get("test").is_some());
200        assert_eq!(manager.list().len(), 1);
201    }
202
203    #[test]
204    fn test_active_workspace() {
205        let mut manager = WorkspaceManager::new();
206
207        let workspace = Workspace::new("test", "Test", PathBuf::from("/tmp"));
208        manager.create(workspace);
209
210        manager.set_active("test").unwrap();
211        assert!(manager.get_active().is_some());
212        assert_eq!(manager.get_active().unwrap().id, "test");
213    }
214
215    #[test]
216    fn test_remove_workspace() {
217        let mut manager = WorkspaceManager::new();
218
219        let workspace = Workspace::new("test", "Test", PathBuf::from("/tmp"));
220        manager.create(workspace);
221
222        manager.set_active("test").unwrap();
223        assert!(manager.remove("test").is_some());
224        assert!(manager.get_active().is_none());
225    }
226
227    #[test]
228    fn test_workspace_save_load() {
229        let workspace = Workspace::new("test", "Test Workspace", PathBuf::from("/tmp/test"))
230            .with_metadata("version", "1.0")
231            .with_context("key", serde_json::json!({"value": 42}));
232
233        let temp_file = tempfile::NamedTempFile::new().unwrap();
234        let path = temp_file.path();
235
236        // Save workspace
237        workspace.save_to_file(path).unwrap();
238
239        // Load workspace
240        let loaded = Workspace::load_from_file(path).unwrap();
241
242        assert_eq!(workspace.id, loaded.id);
243        assert_eq!(workspace.name, loaded.name);
244        assert_eq!(workspace.metadata, loaded.metadata);
245        assert_eq!(workspace.context, loaded.context);
246    }
247}