steer_workspace/
lib.rs

1pub mod config;
2pub mod error;
3pub mod local;
4pub mod utils;
5
6// Re-export main types
7pub use config::{RemoteAuth, WorkspaceConfig};
8pub use error::{Result, WorkspaceError};
9
10// Module with the trait and core types
11use async_trait::async_trait;
12use serde::{Deserialize, Serialize};
13use std::time::{Duration, Instant};
14
15/// Core workspace abstraction for environment information and file operations
16#[async_trait]
17pub trait Workspace: Send + Sync + std::fmt::Debug {
18    /// Get environment information for this workspace
19    async fn environment(&self) -> Result<EnvironmentInfo>;
20
21    /// Get workspace metadata
22    fn metadata(&self) -> WorkspaceMetadata;
23
24    /// Invalidate cached environment information (force refresh on next call)
25    async fn invalidate_environment_cache(&self);
26
27    /// List files in the workspace for fuzzy finding
28    /// Returns workspace-relative paths, filtered by optional query
29    async fn list_files(
30        &self,
31        query: Option<&str>,
32        max_results: Option<usize>,
33    ) -> Result<Vec<String>>;
34
35    /// Get the working directory for this workspace
36    fn working_directory(&self) -> &std::path::Path;
37
38    /// Execute a tool in this workspace
39    async fn execute_tool(
40        &self,
41        tool_call: &steer_tools::ToolCall,
42        context: steer_tools::ExecutionContext,
43    ) -> Result<steer_tools::result::ToolResult>;
44
45    /// Get available tools in this workspace
46    async fn available_tools(&self) -> Vec<steer_tools::ToolSchema>;
47
48    /// Check if a tool requires approval
49    async fn requires_approval(&self, tool_name: &str) -> Result<bool>;
50}
51
52/// Metadata about a workspace
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct WorkspaceMetadata {
55    pub id: String,
56    pub workspace_type: WorkspaceType,
57    pub location: String, // local path, remote URL, or container ID
58}
59
60/// Type of workspace
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub enum WorkspaceType {
63    Local,
64    Remote,
65}
66
67impl WorkspaceType {
68    pub fn as_str(&self) -> &'static str {
69        match self {
70            WorkspaceType::Local => "Local",
71            WorkspaceType::Remote => "Remote",
72        }
73    }
74}
75
76/// Cached environment information with TTL
77#[derive(Debug, Clone)]
78pub(crate) struct CachedEnvironment {
79    pub info: EnvironmentInfo,
80    pub cached_at: Instant,
81    pub ttl: Duration,
82}
83
84impl CachedEnvironment {
85    pub fn new(info: EnvironmentInfo, ttl: Duration) -> Self {
86        Self {
87            info,
88            cached_at: Instant::now(),
89            ttl,
90        }
91    }
92
93    pub fn is_expired(&self) -> bool {
94        self.cached_at.elapsed() > self.ttl
95    }
96}
97
98/// Environment information for a workspace
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct EnvironmentInfo {
101    pub working_directory: std::path::PathBuf,
102    pub is_git_repo: bool,
103    pub platform: String,
104    pub date: String,
105    pub directory_structure: String,
106    pub git_status: Option<String>,
107    pub readme_content: Option<String>,
108    pub claude_md_content: Option<String>,
109}
110
111impl EnvironmentInfo {
112    /// Collect environment information for a given path
113    pub fn collect_for_path(path: &std::path::Path) -> Result<Self> {
114        use crate::utils::{DirectoryStructureUtils, EnvironmentUtils, GitStatusUtils};
115
116        let is_git_repo = EnvironmentUtils::is_git_repo(path);
117        let platform = EnvironmentUtils::get_platform().to_string();
118        let date = EnvironmentUtils::get_current_date();
119
120        let directory_structure = DirectoryStructureUtils::get_directory_structure(path, 3)?;
121
122        let git_status = if is_git_repo {
123            GitStatusUtils::get_git_status(path).ok()
124        } else {
125            None
126        };
127
128        let readme_content = EnvironmentUtils::read_readme(path);
129        let claude_md_content = EnvironmentUtils::read_claude_md(path);
130
131        Ok(Self {
132            working_directory: path.to_path_buf(),
133            is_git_repo,
134            platform,
135            date,
136            directory_structure,
137            git_status,
138            readme_content,
139            claude_md_content,
140        })
141    }
142
143    /// Format environment info as context for system prompt
144    pub fn as_context(&self) -> String {
145        let mut context = format!(
146            "Here is useful information about the environment you are running in:\n<env>\nWorking directory: {}\nIs directory a git repo: {}\nPlatform: {}\nToday's date: {}\n</env>",
147            self.working_directory.display(),
148            self.is_git_repo,
149            self.platform,
150            self.date
151        );
152
153        if !self.directory_structure.is_empty() {
154            context.push_str(&format!("\n\n<file_structure>\nBelow is a snapshot of this project's file structure at the start of the conversation. The file structure may be filtered to omit `.gitignore`ed patterns. This snapshot will NOT update during the conversation.\n\n{}\n</file_structure>", self.directory_structure));
155        }
156
157        if let Some(ref git_status) = self.git_status {
158            context.push_str(&format!("\n<git_status>\nThis is the git status at the start of the conversation. Note that this status is a snapshot in time, and will not update during the conversation.\n\n{git_status}\n</git_status>"));
159        }
160
161        if let Some(ref readme) = self.readme_content {
162            context.push_str(&format!("\n<file name=\"README.md\">\nThis is the README.md file at the start of the conversation. Note that this README is a snapshot in time, and will not update during the conversation.\n\n{readme}\n</file>"));
163        }
164
165        if let Some(ref claude_md) = self.claude_md_content {
166            context.push_str(&format!("\n<file name=\"CLAUDE.md\">\nThis is the CLAUDE.md file at the start of the conversation. Note that this CLAUDE is a snapshot in time, and will not update during the conversation.\n\n{claude_md}\n</file>"));
167        }
168
169        context
170    }
171}