1#[cfg(test)]
11pub(crate) mod conformance;
12mod error;
13mod local;
14mod local_access;
15mod manifest;
16mod path;
17mod remote_git;
18pub(crate) mod retrieval;
19#[cfg(feature = "s3")]
20mod s3;
21mod services;
22mod source_egress;
23
24pub use error::{WorkspaceError, WorkspaceResult};
25pub use local::LocalWorkspaceBackend;
26pub use local_access::LocalWorkspaceAccessPolicy;
27pub use manifest::{
28 scan_workspace_files, LocalWorkspaceFile, LocalWorkspaceFileStatus, LocalWorkspaceManifest,
29 LocalWorkspaceManifestSnapshot, ManifestWorkspaceBackend, RecentWorkspaceFile,
30 WorkspaceFileChange, WorkspaceFileChangeKind,
31};
32pub use path::VirtualPathResolver;
33use path::{
34 default_path_input, has_windows_path_prefix, normalize_relative_path, pathbuf_to_workspace_path,
35};
36pub(crate) use path::{escape_control_chars_for_display, validate_relative_pattern};
37pub use remote_git::{RemoteGitBackend, RemoteGitBackendConfig, RemoteGitConflict};
38pub use retrieval::{
39 ChunkCatalogLimits, ChunkCatalogSnapshot, ChunkingConfig, CustomWorkspaceChunkingStrategy,
40 FixedWindowChunkingOptions, LexicalSearchHit, LexicalSearchRequest, LexicalSearchResult,
41 RecursiveChunkingOptions, WorkspaceChunk, WorkspaceChunkCatalog, WorkspaceChunkId,
42 WorkspaceChunkRange, WorkspaceChunkingError, WorkspaceChunkingInput, WorkspaceChunkingStrategy,
43 WorkspaceEligibilityPolicy, WorkspaceEmbeddingBatchMetrics, WorkspaceHybridChannelRank,
44 WorkspaceHybridChannelStatus, WorkspaceHybridFallbackReason, WorkspaceHybridSearchHit,
45 WorkspaceHybridSearchRequest, WorkspaceHybridSearchResult, WorkspaceIndexError,
46 WorkspaceRerankAlgorithm, WorkspaceRerankFallbackReason, WorkspaceRerankMode,
47 WorkspaceRerankOptions, WorkspaceRerankStatus, WorkspaceRetrievalChannel,
48 WorkspaceRetrievalError, WorkspaceRetrievalOptions, WorkspaceRetrievalPhase,
49 WorkspaceRetrievalResult, WorkspaceRetrievalRuntime, WorkspaceRetrievalStatus,
50 WorkspaceSemanticFallbackReason, WorkspaceSemanticIndexLimits, WorkspaceSemanticSearchHit,
51 WorkspaceSemanticSearchRequest, WorkspaceSemanticSearchResult, WorkspaceVecShadowPhase,
52 WorkspaceVecShadowStatus, WorkspaceVectorEngine,
53};
54#[cfg(feature = "s3")]
55pub use s3::{S3BackendConfig, S3WorkspaceBackend};
56pub use services::{WorkspaceServices, WorkspaceServicesBuilder};
57
58use anyhow::Result;
59use async_trait::async_trait;
60use std::collections::HashMap;
61use std::sync::Arc;
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct WorkspaceRef {
66 pub id: String,
68 pub display_root: String,
70}
71
72impl WorkspaceRef {
73 pub fn new(id: impl Into<String>, display_root: impl Into<String>) -> Self {
74 Self {
75 id: id.into(),
76 display_root: display_root.into(),
77 }
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Hash)]
83pub struct WorkspacePath {
84 inner: String,
85}
86
87impl WorkspacePath {
88 pub fn root() -> Self {
89 Self {
90 inner: ".".to_string(),
91 }
92 }
93
94 pub fn from_normalized(path: impl Into<String>) -> Self {
95 let path = path.into();
96 let path = path.trim_matches('/');
97 if path.is_empty() || path == "." {
98 Self::root()
99 } else {
100 Self {
101 inner: path.replace('\\', "/"),
102 }
103 }
104 }
105
106 pub fn as_str(&self) -> &str {
107 &self.inner
108 }
109
110 pub fn is_root(&self) -> bool {
111 self.inner == "."
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub struct WorkspaceCapabilities {
122 pub read: bool,
123 pub write: bool,
124 pub exec: bool,
125 pub search: bool,
126 pub git: bool,
127 pub code_intelligence: bool,
128}
129
130impl WorkspaceCapabilities {
131 pub fn local_default() -> Self {
132 Self {
133 read: true,
134 write: true,
135 exec: true,
136 search: true,
137 git: true,
138 code_intelligence: false,
139 }
140 }
141
142 pub fn read_write() -> Self {
143 Self {
144 read: true,
145 write: true,
146 exec: false,
147 search: false,
148 git: false,
149 code_intelligence: false,
150 }
151 }
152}
153
154impl Default for WorkspaceCapabilities {
155 fn default() -> Self {
156 Self::read_write()
157 }
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub enum WorkspaceFileType {
163 File,
164 Directory,
165 Symlink,
166 Unknown,
167}
168
169impl WorkspaceFileType {
170 pub fn as_tool_kind(self) -> &'static str {
171 match self {
172 Self::File => "file",
173 Self::Directory => "dir",
174 Self::Symlink => "link",
175 Self::Unknown => "unknown",
176 }
177 }
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct WorkspaceDirEntry {
183 pub name: String,
184 pub kind: WorkspaceFileType,
185 pub size: u64,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct WorkspaceWriteOutcome {
191 pub bytes: usize,
192 pub lines: usize,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq)]
197pub struct WorkspaceGlobRequest {
198 pub base: WorkspacePath,
199 pub pattern: String,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct WorkspaceGlobResult {
205 pub matches: Vec<WorkspacePath>,
206}
207
208#[derive(Debug, Clone, PartialEq, Eq)]
210pub struct WorkspaceGrepRequest {
211 pub base: WorkspacePath,
212 pub pattern: String,
213 pub glob: Option<String>,
214 pub context_lines: usize,
215 pub case_insensitive: bool,
216 pub max_output_size: usize,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct WorkspaceGrepResult {
225 pub output: String,
226 pub match_count: usize,
227 pub file_count: usize,
228 pub truncated: bool,
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct WorkspaceGrepOutcome {
234 pub result: WorkspaceGrepResult,
235 pub matched_paths: Option<Vec<WorkspacePath>>,
238}
239
240#[derive(Debug, Clone, PartialEq, Eq)]
242pub struct WorkspaceGitStatus {
243 pub branch: String,
244 pub commit: String,
245 pub is_worktree: bool,
246 pub is_dirty: bool,
247 pub dirty_count: usize,
248}
249
250#[derive(Debug, Clone, PartialEq, Eq)]
252pub struct WorkspaceGitCommit {
253 pub id: String,
254 pub message: String,
255 pub author: String,
256 pub date: String,
257}
258
259#[derive(Debug, Clone, PartialEq, Eq)]
261pub struct WorkspaceGitBranch {
262 pub name: String,
263 pub is_current: bool,
264}
265
266#[derive(Debug, Clone, PartialEq, Eq)]
268pub struct WorkspaceGitCreateBranchRequest {
269 pub name: String,
270 pub base: String,
271}
272
273#[derive(Debug, Clone, PartialEq, Eq)]
275pub struct WorkspaceGitCheckoutRequest {
276 pub refspec: String,
277 pub force: bool,
278}
279
280#[derive(Debug, Clone, PartialEq, Eq)]
282pub struct WorkspaceGitCheckoutOutput {
283 pub stdout: String,
284}
285
286#[derive(Debug, Clone, PartialEq, Eq)]
288pub struct WorkspaceGitDiffRequest {
289 pub target: Option<String>,
290}
291
292#[derive(Debug, Clone, PartialEq, Eq)]
294pub struct WorkspaceGitStash {
295 pub index: usize,
296 pub message: String,
297}
298
299#[derive(Debug, Clone, PartialEq, Eq)]
301pub struct WorkspaceGitStashRequest {
302 pub message: Option<String>,
303 pub include_untracked: bool,
304}
305
306#[derive(Debug, Clone, PartialEq, Eq)]
308pub struct WorkspaceGitRemote {
309 pub name: String,
310 pub url: String,
311 pub direction: String,
312}
313
314#[derive(Debug, Clone, PartialEq, Eq)]
316pub struct WorkspaceGitWorktree {
317 pub path: String,
318 pub branch: String,
319 pub is_bare: bool,
320 pub is_detached: bool,
321}
322
323#[derive(Debug, Clone, PartialEq, Eq)]
325pub struct WorkspaceGitCreateWorktreeRequest {
326 pub branch: String,
327 pub path: Option<String>,
328 pub new_branch: bool,
329}
330
331#[derive(Debug, Clone, PartialEq, Eq)]
333pub struct WorkspaceGitRemoveWorktreeRequest {
334 pub path: String,
335 pub force: bool,
336}
337
338#[derive(Debug, Clone, PartialEq, Eq)]
340pub struct WorkspaceGitWorktreeMutation {
341 pub path: String,
342 pub branch: Option<String>,
343}
344
345#[async_trait]
351pub trait CommandOutputObserver: Send + Sync {
352 async fn on_output_delta(&self, delta: &str);
353
354 async fn on_output_complete(&self, _summary: &CommandOutputSummary) {}
360}
361
362#[derive(Debug, Clone, Copy, PartialEq, Eq)]
364pub struct CommandOutputSummary {
365 pub total_bytes: usize,
367 pub captured_bytes: usize,
369 pub truncated: bool,
371 pub timed_out: bool,
373}
374
375#[derive(Clone)]
377pub struct CommandRequest {
378 pub command: String,
379 pub timeout_ms: u64,
380 pub output_observer: Option<Arc<dyn CommandOutputObserver>>,
381 pub env: Option<Arc<HashMap<String, String>>>,
382}
383
384impl std::fmt::Debug for CommandRequest {
385 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386 f.debug_struct("CommandRequest")
387 .field("command", &self.command)
388 .field("timeout_ms", &self.timeout_ms)
389 .field("output_observer", &self.output_observer.is_some())
390 .field("env", &self.env.as_ref().map(|env| env.len()))
391 .finish()
392 }
393}
394
395#[derive(Debug, Clone, PartialEq, Eq)]
397pub struct CommandOutput {
398 pub output: String,
399 pub exit_code: i32,
400 pub timed_out: bool,
401}
402
403pub trait WorkspacePathResolver: Send + Sync {
405 fn normalize(&self, input: &str) -> Result<WorkspacePath>;
406}
407
408#[async_trait]
418pub trait WorkspaceFileSystem: Send + Sync {
419 async fn read_text(&self, path: &WorkspacePath) -> WorkspaceResult<String>;
420 async fn write_text(
421 &self,
422 path: &WorkspacePath,
423 content: &str,
424 ) -> WorkspaceResult<WorkspaceWriteOutcome>;
425 async fn list_dir(&self, path: &WorkspacePath) -> WorkspaceResult<Vec<WorkspaceDirEntry>>;
426}
427
428#[derive(Debug, Clone, PartialEq, Eq)]
430pub struct WorkspaceTextRange {
431 pub lines: Vec<String>,
432 pub next_offset: Option<usize>,
433 pub eof: bool,
434 pub total_lines: Option<usize>,
436}
437
438#[async_trait]
441pub trait WorkspaceTextReader: Send + Sync {
442 async fn read_text_range(
443 &self,
444 path: &WorkspacePath,
445 offset: usize,
446 limit: usize,
447 ) -> WorkspaceResult<WorkspaceTextRange>;
448}
449
450#[derive(Debug, Clone, thiserror::Error)]
457#[error(
458 "version conflict on {path}: expected version {expected:?}, found {actual:?} (file modified by another writer; re-read and retry)"
459)]
460pub struct WorkspaceVersionConflict {
461 pub path: String,
462 pub expected: String,
463 pub actual: Option<String>,
466}
467
468#[async_trait]
480pub trait WorkspaceFileSystemExt: Send + Sync {
481 async fn read_text_with_version(
486 &self,
487 path: &WorkspacePath,
488 ) -> WorkspaceResult<(String, String)>;
489
490 async fn write_text_if_version(
496 &self,
497 path: &WorkspacePath,
498 content: &str,
499 expected_version: &str,
500 ) -> WorkspaceResult<WorkspaceWriteOutcome>;
501}
502
503#[async_trait]
505pub trait WorkspaceCommandRunner: Send + Sync {
506 async fn exec(&self, request: CommandRequest) -> Result<CommandOutput>;
507}
508
509#[async_trait]
511pub trait WorkspaceSearch: Send + Sync {
512 async fn glob(&self, request: WorkspaceGlobRequest) -> Result<WorkspaceGlobResult>;
513 async fn grep(&self, request: WorkspaceGrepRequest) -> Result<WorkspaceGrepResult>;
514
515 async fn grep_with_sources(
520 &self,
521 request: WorkspaceGrepRequest,
522 ) -> Result<WorkspaceGrepOutcome> {
523 let result = self.grep(request).await?;
524 Ok(WorkspaceGrepOutcome {
525 result,
526 matched_paths: None,
527 })
528 }
529}
530
531#[async_trait]
537pub trait WorkspaceGit: Send + Sync {
538 async fn is_repository(&self) -> Result<bool>;
539 async fn status(&self) -> Result<WorkspaceGitStatus>;
540 async fn log(&self, max_count: usize) -> Result<Vec<WorkspaceGitCommit>>;
541 async fn list_branches(&self) -> Result<Vec<WorkspaceGitBranch>>;
542 async fn create_branch(&self, request: WorkspaceGitCreateBranchRequest) -> Result<()>;
543 async fn checkout(
544 &self,
545 request: WorkspaceGitCheckoutRequest,
546 ) -> Result<WorkspaceGitCheckoutOutput>;
547 async fn diff(&self, request: WorkspaceGitDiffRequest) -> Result<String>;
548 async fn list_remotes(&self) -> Result<Vec<WorkspaceGitRemote>>;
549}
550
551#[async_trait]
556pub trait WorkspaceGitStashProvider: Send + Sync {
557 async fn list_stashes(&self) -> Result<Vec<WorkspaceGitStash>>;
558 async fn stash(&self, request: WorkspaceGitStashRequest) -> Result<()>;
559}
560
561#[async_trait]
566pub trait WorkspaceGitWorktreeProvider: Send + Sync {
567 async fn list_worktrees(&self) -> Result<Vec<WorkspaceGitWorktree>>;
568 async fn create_worktree(
569 &self,
570 request: WorkspaceGitCreateWorktreeRequest,
571 ) -> Result<WorkspaceGitWorktreeMutation>;
572 async fn remove_worktree(
573 &self,
574 request: WorkspaceGitRemoveWorktreeRequest,
575 ) -> Result<WorkspaceGitWorktreeMutation>;
576}
577
578#[cfg(test)]
579#[path = "tests.rs"]
580mod tests;