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