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 use path::VirtualPathResolver;
31use path::{
32 default_path_input, has_windows_path_prefix, normalize_relative_path, pathbuf_to_workspace_path,
33};
34pub(crate) use path::{escape_control_chars_for_display, validate_relative_pattern};
35pub use remote_git::{RemoteGitBackend, RemoteGitBackendConfig, RemoteGitConflict};
36#[cfg(feature = "s3")]
37pub use s3::{S3BackendConfig, S3WorkspaceBackend};
38pub use services::{WorkspaceServices, WorkspaceServicesBuilder};
39
40use anyhow::Result;
41use async_trait::async_trait;
42use std::collections::HashMap;
43use std::sync::Arc;
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct WorkspaceRef {
48 pub id: String,
50 pub display_root: String,
52}
53
54impl WorkspaceRef {
55 pub fn new(id: impl Into<String>, display_root: impl Into<String>) -> Self {
56 Self {
57 id: id.into(),
58 display_root: display_root.into(),
59 }
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Hash)]
65pub struct WorkspacePath {
66 inner: String,
67}
68
69impl WorkspacePath {
70 pub fn root() -> Self {
71 Self {
72 inner: ".".to_string(),
73 }
74 }
75
76 pub fn from_normalized(path: impl Into<String>) -> Self {
77 let path = path.into();
78 let path = path.trim_matches('/');
79 if path.is_empty() || path == "." {
80 Self::root()
81 } else {
82 Self {
83 inner: path.replace('\\', "/"),
84 }
85 }
86 }
87
88 pub fn as_str(&self) -> &str {
89 &self.inner
90 }
91
92 pub fn is_root(&self) -> bool {
93 self.inner == "."
94 }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub struct WorkspaceCapabilities {
104 pub read: bool,
105 pub write: bool,
106 pub exec: bool,
107 pub search: bool,
108 pub git: bool,
109 pub code_intelligence: bool,
110}
111
112impl WorkspaceCapabilities {
113 pub fn local_default() -> Self {
114 Self {
115 read: true,
116 write: true,
117 exec: true,
118 search: true,
119 git: true,
120 code_intelligence: false,
121 }
122 }
123
124 pub fn read_write() -> Self {
125 Self {
126 read: true,
127 write: true,
128 exec: false,
129 search: false,
130 git: false,
131 code_intelligence: false,
132 }
133 }
134}
135
136impl Default for WorkspaceCapabilities {
137 fn default() -> Self {
138 Self::read_write()
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub enum WorkspaceFileType {
145 File,
146 Directory,
147 Symlink,
148 Unknown,
149}
150
151impl WorkspaceFileType {
152 pub fn as_tool_kind(self) -> &'static str {
153 match self {
154 Self::File => "file",
155 Self::Directory => "dir",
156 Self::Symlink => "link",
157 Self::Unknown => "unknown",
158 }
159 }
160}
161
162#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct WorkspaceDirEntry {
165 pub name: String,
166 pub kind: WorkspaceFileType,
167 pub size: u64,
168}
169
170#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct WorkspaceWriteOutcome {
173 pub bytes: usize,
174 pub lines: usize,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
179pub struct WorkspaceGlobRequest {
180 pub base: WorkspacePath,
181 pub pattern: String,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
186pub struct WorkspaceGlobResult {
187 pub matches: Vec<WorkspacePath>,
188}
189
190#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct WorkspaceGrepRequest {
193 pub base: WorkspacePath,
194 pub pattern: String,
195 pub glob: Option<String>,
196 pub context_lines: usize,
197 pub case_insensitive: bool,
198 pub max_output_size: usize,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct WorkspaceGrepResult {
207 pub output: String,
208 pub match_count: usize,
209 pub file_count: usize,
210 pub truncated: bool,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
215pub struct WorkspaceGrepOutcome {
216 pub result: WorkspaceGrepResult,
217 pub matched_paths: Option<Vec<WorkspacePath>>,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct WorkspaceGitStatus {
225 pub branch: String,
226 pub commit: String,
227 pub is_worktree: bool,
228 pub is_dirty: bool,
229 pub dirty_count: usize,
230}
231
232#[derive(Debug, Clone, PartialEq, Eq)]
234pub struct WorkspaceGitCommit {
235 pub id: String,
236 pub message: String,
237 pub author: String,
238 pub date: String,
239}
240
241#[derive(Debug, Clone, PartialEq, Eq)]
243pub struct WorkspaceGitBranch {
244 pub name: String,
245 pub is_current: bool,
246}
247
248#[derive(Debug, Clone, PartialEq, Eq)]
250pub struct WorkspaceGitCreateBranchRequest {
251 pub name: String,
252 pub base: String,
253}
254
255#[derive(Debug, Clone, PartialEq, Eq)]
257pub struct WorkspaceGitCheckoutRequest {
258 pub refspec: String,
259 pub force: bool,
260}
261
262#[derive(Debug, Clone, PartialEq, Eq)]
264pub struct WorkspaceGitCheckoutOutput {
265 pub stdout: String,
266}
267
268#[derive(Debug, Clone, PartialEq, Eq)]
270pub struct WorkspaceGitDiffRequest {
271 pub target: Option<String>,
272}
273
274#[derive(Debug, Clone, PartialEq, Eq)]
276pub struct WorkspaceGitStash {
277 pub index: usize,
278 pub message: String,
279}
280
281#[derive(Debug, Clone, PartialEq, Eq)]
283pub struct WorkspaceGitStashRequest {
284 pub message: Option<String>,
285 pub include_untracked: bool,
286}
287
288#[derive(Debug, Clone, PartialEq, Eq)]
290pub struct WorkspaceGitRemote {
291 pub name: String,
292 pub url: String,
293 pub direction: String,
294}
295
296#[derive(Debug, Clone, PartialEq, Eq)]
298pub struct WorkspaceGitWorktree {
299 pub path: String,
300 pub branch: String,
301 pub is_bare: bool,
302 pub is_detached: bool,
303}
304
305#[derive(Debug, Clone, PartialEq, Eq)]
307pub struct WorkspaceGitCreateWorktreeRequest {
308 pub branch: String,
309 pub path: Option<String>,
310 pub new_branch: bool,
311}
312
313#[derive(Debug, Clone, PartialEq, Eq)]
315pub struct WorkspaceGitRemoveWorktreeRequest {
316 pub path: String,
317 pub force: bool,
318}
319
320#[derive(Debug, Clone, PartialEq, Eq)]
322pub struct WorkspaceGitWorktreeMutation {
323 pub path: String,
324 pub branch: Option<String>,
325}
326
327#[async_trait]
333pub trait CommandOutputObserver: Send + Sync {
334 async fn on_output_delta(&self, delta: &str);
335
336 async fn on_output_complete(&self, _summary: &CommandOutputSummary) {}
342}
343
344#[derive(Debug, Clone, Copy, PartialEq, Eq)]
346pub struct CommandOutputSummary {
347 pub total_bytes: usize,
349 pub captured_bytes: usize,
351 pub truncated: bool,
353 pub timed_out: bool,
355}
356
357#[derive(Clone)]
359pub struct CommandRequest {
360 pub command: String,
361 pub timeout_ms: u64,
362 pub output_observer: Option<Arc<dyn CommandOutputObserver>>,
363 pub env: Option<Arc<HashMap<String, String>>>,
364}
365
366impl std::fmt::Debug for CommandRequest {
367 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368 f.debug_struct("CommandRequest")
369 .field("command", &self.command)
370 .field("timeout_ms", &self.timeout_ms)
371 .field("output_observer", &self.output_observer.is_some())
372 .field("env", &self.env.as_ref().map(|env| env.len()))
373 .finish()
374 }
375}
376
377#[derive(Debug, Clone, PartialEq, Eq)]
379pub struct CommandOutput {
380 pub output: String,
381 pub exit_code: i32,
382 pub timed_out: bool,
383}
384
385pub trait WorkspacePathResolver: Send + Sync {
387 fn normalize(&self, input: &str) -> Result<WorkspacePath>;
388}
389
390#[async_trait]
400pub trait WorkspaceFileSystem: Send + Sync {
401 async fn read_text(&self, path: &WorkspacePath) -> WorkspaceResult<String>;
402 async fn write_text(
403 &self,
404 path: &WorkspacePath,
405 content: &str,
406 ) -> WorkspaceResult<WorkspaceWriteOutcome>;
407 async fn list_dir(&self, path: &WorkspacePath) -> WorkspaceResult<Vec<WorkspaceDirEntry>>;
408}
409
410#[derive(Debug, Clone, PartialEq, Eq)]
412pub struct WorkspaceTextRange {
413 pub lines: Vec<String>,
414 pub next_offset: Option<usize>,
415 pub eof: bool,
416 pub total_lines: Option<usize>,
418}
419
420#[async_trait]
423pub trait WorkspaceTextReader: Send + Sync {
424 async fn read_text_range(
425 &self,
426 path: &WorkspacePath,
427 offset: usize,
428 limit: usize,
429 ) -> WorkspaceResult<WorkspaceTextRange>;
430}
431
432#[derive(Debug, Clone, thiserror::Error)]
439#[error(
440 "version conflict on {path}: expected version {expected:?}, found {actual:?} (file modified by another writer; re-read and retry)"
441)]
442pub struct WorkspaceVersionConflict {
443 pub path: String,
444 pub expected: String,
445 pub actual: Option<String>,
448}
449
450#[async_trait]
462pub trait WorkspaceFileSystemExt: Send + Sync {
463 async fn read_text_with_version(
468 &self,
469 path: &WorkspacePath,
470 ) -> WorkspaceResult<(String, String)>;
471
472 async fn write_text_if_version(
478 &self,
479 path: &WorkspacePath,
480 content: &str,
481 expected_version: &str,
482 ) -> WorkspaceResult<WorkspaceWriteOutcome>;
483}
484
485#[async_trait]
487pub trait WorkspaceCommandRunner: Send + Sync {
488 async fn exec(&self, request: CommandRequest) -> Result<CommandOutput>;
489}
490
491#[async_trait]
493pub trait WorkspaceSearch: Send + Sync {
494 async fn glob(&self, request: WorkspaceGlobRequest) -> Result<WorkspaceGlobResult>;
495 async fn grep(&self, request: WorkspaceGrepRequest) -> Result<WorkspaceGrepResult>;
496
497 async fn grep_with_sources(
502 &self,
503 request: WorkspaceGrepRequest,
504 ) -> Result<WorkspaceGrepOutcome> {
505 let result = self.grep(request).await?;
506 Ok(WorkspaceGrepOutcome {
507 result,
508 matched_paths: None,
509 })
510 }
511}
512
513#[async_trait]
519pub trait WorkspaceGit: Send + Sync {
520 async fn is_repository(&self) -> Result<bool>;
521 async fn status(&self) -> Result<WorkspaceGitStatus>;
522 async fn log(&self, max_count: usize) -> Result<Vec<WorkspaceGitCommit>>;
523 async fn list_branches(&self) -> Result<Vec<WorkspaceGitBranch>>;
524 async fn create_branch(&self, request: WorkspaceGitCreateBranchRequest) -> Result<()>;
525 async fn checkout(
526 &self,
527 request: WorkspaceGitCheckoutRequest,
528 ) -> Result<WorkspaceGitCheckoutOutput>;
529 async fn diff(&self, request: WorkspaceGitDiffRequest) -> Result<String>;
530 async fn list_remotes(&self) -> Result<Vec<WorkspaceGitRemote>>;
531}
532
533#[async_trait]
538pub trait WorkspaceGitStashProvider: Send + Sync {
539 async fn list_stashes(&self) -> Result<Vec<WorkspaceGitStash>>;
540 async fn stash(&self, request: WorkspaceGitStashRequest) -> Result<()>;
541}
542
543#[async_trait]
548pub trait WorkspaceGitWorktreeProvider: Send + Sync {
549 async fn list_worktrees(&self) -> Result<Vec<WorkspaceGitWorktree>>;
550 async fn create_worktree(
551 &self,
552 request: WorkspaceGitCreateWorktreeRequest,
553 ) -> Result<WorkspaceGitWorktreeMutation>;
554 async fn remove_worktree(
555 &self,
556 request: WorkspaceGitRemoveWorktreeRequest,
557 ) -> Result<WorkspaceGitWorktreeMutation>;
558}
559
560#[cfg(test)]
561#[path = "tests.rs"]
562mod tests;