1pub mod config;
2pub mod error;
3pub mod local;
4pub mod utils;
5
6pub use config::{RemoteAuth, WorkspaceConfig};
8pub use error::{Result, WorkspaceError};
9
10use async_trait::async_trait;
12use serde::{Deserialize, Serialize};
13use std::time::{Duration, Instant};
14use tracing::debug;
15
16#[async_trait]
18pub trait Workspace: Send + Sync + std::fmt::Debug {
19 async fn environment(&self) -> Result<EnvironmentInfo>;
21
22 fn metadata(&self) -> WorkspaceMetadata;
24
25 async fn invalidate_environment_cache(&self);
27
28 async fn list_files(
31 &self,
32 query: Option<&str>,
33 max_results: Option<usize>,
34 ) -> Result<Vec<String>>;
35
36 fn working_directory(&self) -> &std::path::Path;
38
39 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 async fn available_tools(&self) -> Vec<steer_tools::ToolSchema>;
48
49 async fn requires_approval(&self, tool_name: &str) -> Result<bool>;
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct WorkspaceMetadata {
56 pub id: String,
57 pub workspace_type: WorkspaceType,
58 pub location: String, }
60
61#[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#[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#[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
112pub const MAX_DIRECTORY_DEPTH: usize = 3;
114
115pub const MAX_DIRECTORY_ITEMS: usize = 1000;
117
118impl EnvironmentInfo {
119 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 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}