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};
14use tracing::debug;
15
16/// Core workspace abstraction for environment information and file operations
17#[async_trait]
18pub trait Workspace: Send + Sync + std::fmt::Debug {
19    /// Get environment information for this workspace
20    async fn environment(&self) -> Result<EnvironmentInfo>;
21
22    /// Get workspace metadata
23    fn metadata(&self) -> WorkspaceMetadata;
24
25    /// Invalidate cached environment information (force refresh on next call)
26    async fn invalidate_environment_cache(&self);
27
28    /// List files in the workspace for fuzzy finding
29    /// Returns workspace-relative paths, filtered by optional query
30    async fn list_files(
31        &self,
32        query: Option<&str>,
33        max_results: Option<usize>,
34    ) -> Result<Vec<String>>;
35
36    /// Get the working directory for this workspace
37    fn working_directory(&self) -> &std::path::Path;
38
39    /// Execute a tool in this workspace
40    async fn execute_tool(
41        &self,
42        tool_call: &steer_tools::ToolCall,
43        context: steer_tools::ExecutionContext,
44    ) -> Result<steer_tools::result::ToolResult>;
45
46    /// Get available tools in this workspace
47    async fn available_tools(&self) -> Vec<steer_tools::ToolSchema>;
48
49    /// Check if a tool requires approval
50    async fn requires_approval(&self, tool_name: &str) -> Result<bool>;
51}
52
53/// Metadata about a workspace
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct WorkspaceMetadata {
56    pub id: String,
57    pub workspace_type: WorkspaceType,
58    pub location: String, // local path, remote URL, or container ID
59}
60
61/// Type of workspace
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub enum WorkspaceType {
64    Local,
65    Remote,
66}
67
68impl WorkspaceType {
69    pub fn as_str(&self) -> &'static str {
70        match self {
71            WorkspaceType::Local => "Local",
72            WorkspaceType::Remote => "Remote",
73        }
74    }
75}
76
77/// Cached environment information with TTL
78#[derive(Debug, Clone)]
79pub(crate) struct CachedEnvironment {
80    pub info: EnvironmentInfo,
81    pub cached_at: Instant,
82    pub ttl: Duration,
83}
84
85impl CachedEnvironment {
86    pub fn new(info: EnvironmentInfo, ttl: Duration) -> Self {
87        Self {
88            info,
89            cached_at: Instant::now(),
90            ttl,
91        }
92    }
93
94    pub fn is_expired(&self) -> bool {
95        self.cached_at.elapsed() > self.ttl
96    }
97}
98
99/// Environment information for a workspace
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct EnvironmentInfo {
102    pub working_directory: std::path::PathBuf,
103    pub is_git_repo: bool,
104    pub platform: String,
105    pub date: String,
106    pub directory_structure: String,
107    pub git_status: Option<String>,
108    pub readme_content: Option<String>,
109    pub claude_md_content: Option<String>,
110}
111
112/// Default maximum depth for directory structure traversal
113pub const MAX_DIRECTORY_DEPTH: usize = 3;
114
115/// Default maximum number of items to include in directory structure
116pub const MAX_DIRECTORY_ITEMS: usize = 1000;
117
118impl EnvironmentInfo {
119    /// Collect environment information for a given path
120    pub fn collect_for_path(path: &std::path::Path) -> Result<Self> {
121        use crate::utils::{DirectoryStructureUtils, EnvironmentUtils, GitStatusUtils};
122
123        let is_git_repo = EnvironmentUtils::is_git_repo(path);
124        let platform = EnvironmentUtils::get_platform().to_string();
125        let date = EnvironmentUtils::get_current_date();
126
127        let directory_structure = DirectoryStructureUtils::get_directory_structure(
128            path,
129            MAX_DIRECTORY_DEPTH,
130            Some(MAX_DIRECTORY_ITEMS),
131        )?;
132        debug!("directory_structure: {}", directory_structure);
133
134        let git_status = if is_git_repo {
135            GitStatusUtils::get_git_status(path).ok()
136        } else {
137            None
138        };
139
140        let readme_content = EnvironmentUtils::read_readme(path);
141        let claude_md_content = EnvironmentUtils::read_claude_md(path);
142
143        Ok(Self {
144            working_directory: path.to_path_buf(),
145            is_git_repo,
146            platform,
147            date,
148            directory_structure,
149            git_status,
150            readme_content,
151            claude_md_content,
152        })
153    }
154
155    /// Format environment info as context for system prompt
156    pub fn as_context(&self) -> String {
157        let mut context = format!(
158            "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>",
159            self.working_directory.display(),
160            self.is_git_repo,
161            self.platform,
162            self.date
163        );
164
165        if !self.directory_structure.is_empty() {
166            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));
167        }
168
169        if let Some(ref git_status) = self.git_status {
170            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>"));
171        }
172
173        if let Some(ref readme) = self.readme_content {
174            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>"));
175        }
176
177        if let Some(ref claude_md) = self.claude_md_content {
178            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>"));
179        }
180
181        context
182    }
183}