1#[cfg(test)]
11pub(crate) mod conformance;
12mod error;
13mod local;
14mod local_access;
15mod manifest;
16mod path;
17mod remote_git;
18#[cfg(feature = "s3")]
19mod s3;
20mod services;
21
22pub use error::{WorkspaceError, WorkspaceResult};
23pub use local::LocalWorkspaceBackend;
24pub use local_access::LocalWorkspaceAccessPolicy;
25pub use manifest::{
26 scan_workspace_files, LocalWorkspaceFile, LocalWorkspaceFileStatus, LocalWorkspaceManifest,
27 LocalWorkspaceManifestSnapshot, ManifestWorkspaceBackend, RecentWorkspaceFile,
28 WorkspaceFileChange, WorkspaceFileChangeKind,
29};
30pub(crate) use path::validate_relative_pattern;
31pub use path::VirtualPathResolver;
32use path::{
33 default_path_input, escape_control_chars_for_display, has_windows_path_prefix,
34 normalize_relative_path, pathbuf_to_workspace_path,
35};
36pub use remote_git::{RemoteGitBackend, RemoteGitBackendConfig, RemoteGitConflict};
37#[cfg(feature = "s3")]
38pub use s3::{S3BackendConfig, S3WorkspaceBackend};
39pub use services::{WorkspaceServices, WorkspaceServicesBuilder};
40
41use anyhow::Result;
42use async_trait::async_trait;
43use std::collections::HashMap;
44use std::sync::Arc;
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct WorkspaceRef {
49 pub id: String,
51 pub display_root: String,
53}
54
55impl WorkspaceRef {
56 pub fn new(id: impl Into<String>, display_root: impl Into<String>) -> Self {
57 Self {
58 id: id.into(),
59 display_root: display_root.into(),
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Hash)]
66pub struct WorkspacePath {
67 inner: String,
68}
69
70impl WorkspacePath {
71 pub fn root() -> Self {
72 Self {
73 inner: ".".to_string(),
74 }
75 }
76
77 pub fn from_normalized(path: impl Into<String>) -> Self {
78 let path = path.into();
79 let path = path.trim_matches('/');
80 if path.is_empty() || path == "." {
81 Self::root()
82 } else {
83 Self {
84 inner: path.replace('\\', "/"),
85 }
86 }
87 }
88
89 pub fn as_str(&self) -> &str {
90 &self.inner
91 }
92
93 pub fn is_root(&self) -> bool {
94 self.inner == "."
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub struct WorkspaceCapabilities {
105 pub read: bool,
106 pub write: bool,
107 pub exec: bool,
108 pub search: bool,
109 pub git: bool,
110 pub code_intelligence: bool,
111}
112
113impl WorkspaceCapabilities {
114 pub fn local_default() -> Self {
115 Self {
116 read: true,
117 write: true,
118 exec: true,
119 search: true,
120 git: true,
121 code_intelligence: false,
122 }
123 }
124
125 pub fn read_write() -> Self {
126 Self {
127 read: true,
128 write: true,
129 exec: false,
130 search: false,
131 git: false,
132 code_intelligence: false,
133 }
134 }
135}
136
137impl Default for WorkspaceCapabilities {
138 fn default() -> Self {
139 Self::read_write()
140 }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub enum WorkspaceFileType {
146 File,
147 Directory,
148 Symlink,
149 Unknown,
150}
151
152impl WorkspaceFileType {
153 pub fn as_tool_kind(self) -> &'static str {
154 match self {
155 Self::File => "file",
156 Self::Directory => "dir",
157 Self::Symlink => "link",
158 Self::Unknown => "unknown",
159 }
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct WorkspaceDirEntry {
166 pub name: String,
167 pub kind: WorkspaceFileType,
168 pub size: u64,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct WorkspaceWriteOutcome {
174 pub bytes: usize,
175 pub lines: usize,
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct WorkspaceGlobRequest {
181 pub base: WorkspacePath,
182 pub pattern: String,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq)]
187pub struct WorkspaceGlobResult {
188 pub matches: Vec<WorkspacePath>,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct WorkspaceGrepRequest {
194 pub base: WorkspacePath,
195 pub pattern: String,
196 pub glob: Option<String>,
197 pub context_lines: usize,
198 pub case_insensitive: bool,
199 pub max_output_size: usize,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct WorkspaceGrepResult {
205 pub output: String,
206 pub match_count: usize,
207 pub file_count: usize,
208 pub truncated: bool,
209}
210
211#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct WorkspaceGrepOutcome {
214 pub result: WorkspaceGrepResult,
215 pub matched_paths: Option<Vec<WorkspacePath>>,
218}
219
220#[derive(Debug, Clone, PartialEq, Eq)]
222pub struct WorkspaceGitStatus {
223 pub branch: String,
224 pub commit: String,
225 pub is_worktree: bool,
226 pub is_dirty: bool,
227 pub dirty_count: usize,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq)]
232pub struct WorkspaceGitCommit {
233 pub id: String,
234 pub message: String,
235 pub author: String,
236 pub date: String,
237}
238
239#[derive(Debug, Clone, PartialEq, Eq)]
241pub struct WorkspaceGitBranch {
242 pub name: String,
243 pub is_current: bool,
244}
245
246#[derive(Debug, Clone, PartialEq, Eq)]
248pub struct WorkspaceGitCreateBranchRequest {
249 pub name: String,
250 pub base: String,
251}
252
253#[derive(Debug, Clone, PartialEq, Eq)]
255pub struct WorkspaceGitCheckoutRequest {
256 pub refspec: String,
257 pub force: bool,
258}
259
260#[derive(Debug, Clone, PartialEq, Eq)]
262pub struct WorkspaceGitCheckoutOutput {
263 pub stdout: String,
264}
265
266#[derive(Debug, Clone, PartialEq, Eq)]
268pub struct WorkspaceGitDiffRequest {
269 pub target: Option<String>,
270}
271
272#[derive(Debug, Clone, PartialEq, Eq)]
274pub struct WorkspaceGitStash {
275 pub index: usize,
276 pub message: String,
277}
278
279#[derive(Debug, Clone, PartialEq, Eq)]
281pub struct WorkspaceGitStashRequest {
282 pub message: Option<String>,
283 pub include_untracked: bool,
284}
285
286#[derive(Debug, Clone, PartialEq, Eq)]
288pub struct WorkspaceGitRemote {
289 pub name: String,
290 pub url: String,
291 pub direction: String,
292}
293
294#[derive(Debug, Clone, PartialEq, Eq)]
296pub struct WorkspaceGitWorktree {
297 pub path: String,
298 pub branch: String,
299 pub is_bare: bool,
300 pub is_detached: bool,
301}
302
303#[derive(Debug, Clone, PartialEq, Eq)]
305pub struct WorkspaceGitCreateWorktreeRequest {
306 pub branch: String,
307 pub path: Option<String>,
308 pub new_branch: bool,
309}
310
311#[derive(Debug, Clone, PartialEq, Eq)]
313pub struct WorkspaceGitRemoveWorktreeRequest {
314 pub path: String,
315 pub force: bool,
316}
317
318#[derive(Debug, Clone, PartialEq, Eq)]
320pub struct WorkspaceGitWorktreeMutation {
321 pub path: String,
322 pub branch: Option<String>,
323}
324
325#[async_trait]
331pub trait CommandOutputObserver: Send + Sync {
332 async fn on_output_delta(&self, delta: &str);
333
334 async fn on_output_complete(&self, _summary: &CommandOutputSummary) {}
340}
341
342#[derive(Debug, Clone, Copy, PartialEq, Eq)]
344pub struct CommandOutputSummary {
345 pub total_bytes: usize,
347 pub captured_bytes: usize,
349 pub truncated: bool,
351 pub timed_out: bool,
353}
354
355#[derive(Clone)]
357pub struct CommandRequest {
358 pub command: String,
359 pub timeout_ms: u64,
360 pub output_observer: Option<Arc<dyn CommandOutputObserver>>,
361 pub env: Option<Arc<HashMap<String, String>>>,
362}
363
364impl std::fmt::Debug for CommandRequest {
365 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
366 f.debug_struct("CommandRequest")
367 .field("command", &self.command)
368 .field("timeout_ms", &self.timeout_ms)
369 .field("output_observer", &self.output_observer.is_some())
370 .field("env", &self.env.as_ref().map(|env| env.len()))
371 .finish()
372 }
373}
374
375#[derive(Debug, Clone, PartialEq, Eq)]
377pub struct CommandOutput {
378 pub output: String,
379 pub exit_code: i32,
380 pub timed_out: bool,
381}
382
383pub trait WorkspacePathResolver: Send + Sync {
385 fn normalize(&self, input: &str) -> Result<WorkspacePath>;
386}
387
388#[async_trait]
398pub trait WorkspaceFileSystem: Send + Sync {
399 async fn read_text(&self, path: &WorkspacePath) -> WorkspaceResult<String>;
400 async fn write_text(
401 &self,
402 path: &WorkspacePath,
403 content: &str,
404 ) -> WorkspaceResult<WorkspaceWriteOutcome>;
405 async fn list_dir(&self, path: &WorkspacePath) -> WorkspaceResult<Vec<WorkspaceDirEntry>>;
406}
407
408#[derive(Debug, Clone, PartialEq, Eq)]
410pub struct WorkspaceTextRange {
411 pub lines: Vec<String>,
412 pub next_offset: Option<usize>,
413 pub eof: bool,
414 pub total_lines: Option<usize>,
416}
417
418#[async_trait]
421pub trait WorkspaceTextReader: Send + Sync {
422 async fn read_text_range(
423 &self,
424 path: &WorkspacePath,
425 offset: usize,
426 limit: usize,
427 ) -> WorkspaceResult<WorkspaceTextRange>;
428}
429
430#[derive(Debug, Clone, thiserror::Error)]
437#[error(
438 "version conflict on {path}: expected version {expected:?}, found {actual:?} (file modified by another writer; re-read and retry)"
439)]
440pub struct WorkspaceVersionConflict {
441 pub path: String,
442 pub expected: String,
443 pub actual: Option<String>,
446}
447
448#[async_trait]
460pub trait WorkspaceFileSystemExt: Send + Sync {
461 async fn read_text_with_version(
466 &self,
467 path: &WorkspacePath,
468 ) -> WorkspaceResult<(String, String)>;
469
470 async fn write_text_if_version(
476 &self,
477 path: &WorkspacePath,
478 content: &str,
479 expected_version: &str,
480 ) -> WorkspaceResult<WorkspaceWriteOutcome>;
481}
482
483#[async_trait]
485pub trait WorkspaceCommandRunner: Send + Sync {
486 async fn exec(&self, request: CommandRequest) -> Result<CommandOutput>;
487}
488
489#[async_trait]
491pub trait WorkspaceSearch: Send + Sync {
492 async fn glob(&self, request: WorkspaceGlobRequest) -> Result<WorkspaceGlobResult>;
493 async fn grep(&self, request: WorkspaceGrepRequest) -> Result<WorkspaceGrepResult>;
494
495 async fn grep_with_sources(
500 &self,
501 request: WorkspaceGrepRequest,
502 ) -> Result<WorkspaceGrepOutcome> {
503 let result = self.grep(request).await?;
504 Ok(WorkspaceGrepOutcome {
505 result,
506 matched_paths: None,
507 })
508 }
509}
510
511#[async_trait]
517pub trait WorkspaceGit: Send + Sync {
518 async fn is_repository(&self) -> Result<bool>;
519 async fn status(&self) -> Result<WorkspaceGitStatus>;
520 async fn log(&self, max_count: usize) -> Result<Vec<WorkspaceGitCommit>>;
521 async fn list_branches(&self) -> Result<Vec<WorkspaceGitBranch>>;
522 async fn create_branch(&self, request: WorkspaceGitCreateBranchRequest) -> Result<()>;
523 async fn checkout(
524 &self,
525 request: WorkspaceGitCheckoutRequest,
526 ) -> Result<WorkspaceGitCheckoutOutput>;
527 async fn diff(&self, request: WorkspaceGitDiffRequest) -> Result<String>;
528 async fn list_remotes(&self) -> Result<Vec<WorkspaceGitRemote>>;
529}
530
531#[async_trait]
536pub trait WorkspaceGitStashProvider: Send + Sync {
537 async fn list_stashes(&self) -> Result<Vec<WorkspaceGitStash>>;
538 async fn stash(&self, request: WorkspaceGitStashRequest) -> Result<()>;
539}
540
541#[async_trait]
546pub trait WorkspaceGitWorktreeProvider: Send + Sync {
547 async fn list_worktrees(&self) -> Result<Vec<WorkspaceGitWorktree>>;
548 async fn create_worktree(
549 &self,
550 request: WorkspaceGitCreateWorktreeRequest,
551 ) -> Result<WorkspaceGitWorktreeMutation>;
552 async fn remove_worktree(
553 &self,
554 request: WorkspaceGitRemoveWorktreeRequest,
555 ) -> Result<WorkspaceGitWorktreeMutation>;
556}
557
558#[cfg(test)]
559#[path = "tests.rs"]
560mod tests;