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,
52};
53#[cfg(feature = "s3")]
54pub use s3::{S3BackendConfig, S3WorkspaceBackend};
55pub use services::{WorkspaceServices, WorkspaceServicesBuilder};
56
57use anyhow::Result;
58use async_trait::async_trait;
59use std::collections::HashMap;
60use std::sync::Arc;
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct WorkspaceRef {
65 pub id: String,
67 pub display_root: String,
69}
70
71impl WorkspaceRef {
72 pub fn new(id: impl Into<String>, display_root: impl Into<String>) -> Self {
73 Self {
74 id: id.into(),
75 display_root: display_root.into(),
76 }
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Hash)]
82pub struct WorkspacePath {
83 inner: String,
84}
85
86impl WorkspacePath {
87 pub fn root() -> Self {
88 Self {
89 inner: ".".to_string(),
90 }
91 }
92
93 pub fn from_normalized(path: impl Into<String>) -> Self {
94 let path = path.into();
95 let path = path.trim_matches('/');
96 if path.is_empty() || path == "." {
97 Self::root()
98 } else {
99 Self {
100 inner: path.replace('\\', "/"),
101 }
102 }
103 }
104
105 pub fn as_str(&self) -> &str {
106 &self.inner
107 }
108
109 pub fn is_root(&self) -> bool {
110 self.inner == "."
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub struct WorkspaceCapabilities {
121 pub read: bool,
122 pub write: bool,
123 pub exec: bool,
124 pub search: bool,
125 pub git: bool,
126 pub code_intelligence: bool,
127}
128
129impl WorkspaceCapabilities {
130 pub fn local_default() -> Self {
131 Self {
132 read: true,
133 write: true,
134 exec: true,
135 search: true,
136 git: true,
137 code_intelligence: false,
138 }
139 }
140
141 pub fn read_write() -> Self {
142 Self {
143 read: true,
144 write: true,
145 exec: false,
146 search: false,
147 git: false,
148 code_intelligence: false,
149 }
150 }
151}
152
153impl Default for WorkspaceCapabilities {
154 fn default() -> Self {
155 Self::read_write()
156 }
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum WorkspaceFileType {
162 File,
163 Directory,
164 Symlink,
165 Unknown,
166}
167
168impl WorkspaceFileType {
169 pub fn as_tool_kind(self) -> &'static str {
170 match self {
171 Self::File => "file",
172 Self::Directory => "dir",
173 Self::Symlink => "link",
174 Self::Unknown => "unknown",
175 }
176 }
177}
178
179#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct WorkspaceDirEntry {
182 pub name: String,
183 pub kind: WorkspaceFileType,
184 pub size: u64,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct WorkspaceWriteOutcome {
190 pub bytes: usize,
191 pub lines: usize,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct WorkspaceGlobRequest {
197 pub base: WorkspacePath,
198 pub pattern: String,
199}
200
201#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct WorkspaceGlobResult {
204 pub matches: Vec<WorkspacePath>,
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
209pub struct WorkspaceGrepRequest {
210 pub base: WorkspacePath,
211 pub pattern: String,
212 pub glob: Option<String>,
213 pub context_lines: usize,
214 pub case_insensitive: bool,
215 pub max_output_size: usize,
219}
220
221#[derive(Debug, Clone, PartialEq, Eq)]
223pub struct WorkspaceGrepResult {
224 pub output: String,
225 pub match_count: usize,
226 pub file_count: usize,
227 pub truncated: bool,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq)]
232pub struct WorkspaceGrepOutcome {
233 pub result: WorkspaceGrepResult,
234 pub matched_paths: Option<Vec<WorkspacePath>>,
237}
238
239#[derive(Debug, Clone, PartialEq, Eq)]
241pub struct WorkspaceGitStatus {
242 pub branch: String,
243 pub commit: String,
244 pub is_worktree: bool,
245 pub is_dirty: bool,
246 pub dirty_count: usize,
247}
248
249#[derive(Debug, Clone, PartialEq, Eq)]
251pub struct WorkspaceGitCommit {
252 pub id: String,
253 pub message: String,
254 pub author: String,
255 pub date: String,
256}
257
258#[derive(Debug, Clone, PartialEq, Eq)]
260pub struct WorkspaceGitBranch {
261 pub name: String,
262 pub is_current: bool,
263}
264
265#[derive(Debug, Clone, PartialEq, Eq)]
267pub struct WorkspaceGitCreateBranchRequest {
268 pub name: String,
269 pub base: String,
270}
271
272#[derive(Debug, Clone, PartialEq, Eq)]
274pub struct WorkspaceGitCheckoutRequest {
275 pub refspec: String,
276 pub force: bool,
277}
278
279#[derive(Debug, Clone, PartialEq, Eq)]
281pub struct WorkspaceGitCheckoutOutput {
282 pub stdout: String,
283}
284
285#[derive(Debug, Clone, PartialEq, Eq)]
287pub struct WorkspaceGitDiffRequest {
288 pub target: Option<String>,
289}
290
291#[derive(Debug, Clone, PartialEq, Eq)]
293pub struct WorkspaceGitStash {
294 pub index: usize,
295 pub message: String,
296}
297
298#[derive(Debug, Clone, PartialEq, Eq)]
300pub struct WorkspaceGitStashRequest {
301 pub message: Option<String>,
302 pub include_untracked: bool,
303}
304
305#[derive(Debug, Clone, PartialEq, Eq)]
307pub struct WorkspaceGitRemote {
308 pub name: String,
309 pub url: String,
310 pub direction: String,
311}
312
313#[derive(Debug, Clone, PartialEq, Eq)]
315pub struct WorkspaceGitWorktree {
316 pub path: String,
317 pub branch: String,
318 pub is_bare: bool,
319 pub is_detached: bool,
320}
321
322#[derive(Debug, Clone, PartialEq, Eq)]
324pub struct WorkspaceGitCreateWorktreeRequest {
325 pub branch: String,
326 pub path: Option<String>,
327 pub new_branch: bool,
328}
329
330#[derive(Debug, Clone, PartialEq, Eq)]
332pub struct WorkspaceGitRemoveWorktreeRequest {
333 pub path: String,
334 pub force: bool,
335}
336
337#[derive(Debug, Clone, PartialEq, Eq)]
339pub struct WorkspaceGitWorktreeMutation {
340 pub path: String,
341 pub branch: Option<String>,
342}
343
344#[async_trait]
350pub trait CommandOutputObserver: Send + Sync {
351 async fn on_output_delta(&self, delta: &str);
352
353 async fn on_output_complete(&self, _summary: &CommandOutputSummary) {}
359}
360
361#[derive(Debug, Clone, Copy, PartialEq, Eq)]
363pub struct CommandOutputSummary {
364 pub total_bytes: usize,
366 pub captured_bytes: usize,
368 pub truncated: bool,
370 pub timed_out: bool,
372}
373
374#[derive(Clone)]
376pub struct CommandRequest {
377 pub command: String,
378 pub timeout_ms: u64,
379 pub output_observer: Option<Arc<dyn CommandOutputObserver>>,
380 pub env: Option<Arc<HashMap<String, String>>>,
381}
382
383impl std::fmt::Debug for CommandRequest {
384 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
385 f.debug_struct("CommandRequest")
386 .field("command", &self.command)
387 .field("timeout_ms", &self.timeout_ms)
388 .field("output_observer", &self.output_observer.is_some())
389 .field("env", &self.env.as_ref().map(|env| env.len()))
390 .finish()
391 }
392}
393
394#[derive(Debug, Clone, PartialEq, Eq)]
396pub struct CommandOutput {
397 pub output: String,
398 pub exit_code: i32,
399 pub timed_out: bool,
400}
401
402pub trait WorkspacePathResolver: Send + Sync {
404 fn normalize(&self, input: &str) -> Result<WorkspacePath>;
405}
406
407#[async_trait]
417pub trait WorkspaceFileSystem: Send + Sync {
418 async fn read_text(&self, path: &WorkspacePath) -> WorkspaceResult<String>;
419 async fn write_text(
420 &self,
421 path: &WorkspacePath,
422 content: &str,
423 ) -> WorkspaceResult<WorkspaceWriteOutcome>;
424 async fn list_dir(&self, path: &WorkspacePath) -> WorkspaceResult<Vec<WorkspaceDirEntry>>;
425}
426
427#[derive(Debug, Clone, PartialEq, Eq)]
429pub struct WorkspaceTextRange {
430 pub lines: Vec<String>,
431 pub next_offset: Option<usize>,
432 pub eof: bool,
433 pub total_lines: Option<usize>,
435}
436
437#[async_trait]
440pub trait WorkspaceTextReader: Send + Sync {
441 async fn read_text_range(
442 &self,
443 path: &WorkspacePath,
444 offset: usize,
445 limit: usize,
446 ) -> WorkspaceResult<WorkspaceTextRange>;
447}
448
449#[derive(Debug, Clone, thiserror::Error)]
456#[error(
457 "version conflict on {path}: expected version {expected:?}, found {actual:?} (file modified by another writer; re-read and retry)"
458)]
459pub struct WorkspaceVersionConflict {
460 pub path: String,
461 pub expected: String,
462 pub actual: Option<String>,
465}
466
467#[async_trait]
479pub trait WorkspaceFileSystemExt: Send + Sync {
480 async fn read_text_with_version(
485 &self,
486 path: &WorkspacePath,
487 ) -> WorkspaceResult<(String, String)>;
488
489 async fn write_text_if_version(
495 &self,
496 path: &WorkspacePath,
497 content: &str,
498 expected_version: &str,
499 ) -> WorkspaceResult<WorkspaceWriteOutcome>;
500}
501
502#[async_trait]
504pub trait WorkspaceCommandRunner: Send + Sync {
505 async fn exec(&self, request: CommandRequest) -> Result<CommandOutput>;
506}
507
508#[async_trait]
510pub trait WorkspaceSearch: Send + Sync {
511 async fn glob(&self, request: WorkspaceGlobRequest) -> Result<WorkspaceGlobResult>;
512 async fn grep(&self, request: WorkspaceGrepRequest) -> Result<WorkspaceGrepResult>;
513
514 async fn grep_with_sources(
519 &self,
520 request: WorkspaceGrepRequest,
521 ) -> Result<WorkspaceGrepOutcome> {
522 let result = self.grep(request).await?;
523 Ok(WorkspaceGrepOutcome {
524 result,
525 matched_paths: None,
526 })
527 }
528}
529
530#[async_trait]
536pub trait WorkspaceGit: Send + Sync {
537 async fn is_repository(&self) -> Result<bool>;
538 async fn status(&self) -> Result<WorkspaceGitStatus>;
539 async fn log(&self, max_count: usize) -> Result<Vec<WorkspaceGitCommit>>;
540 async fn list_branches(&self) -> Result<Vec<WorkspaceGitBranch>>;
541 async fn create_branch(&self, request: WorkspaceGitCreateBranchRequest) -> Result<()>;
542 async fn checkout(
543 &self,
544 request: WorkspaceGitCheckoutRequest,
545 ) -> Result<WorkspaceGitCheckoutOutput>;
546 async fn diff(&self, request: WorkspaceGitDiffRequest) -> Result<String>;
547 async fn list_remotes(&self) -> Result<Vec<WorkspaceGitRemote>>;
548}
549
550#[async_trait]
555pub trait WorkspaceGitStashProvider: Send + Sync {
556 async fn list_stashes(&self) -> Result<Vec<WorkspaceGitStash>>;
557 async fn stash(&self, request: WorkspaceGitStashRequest) -> Result<()>;
558}
559
560#[async_trait]
565pub trait WorkspaceGitWorktreeProvider: Send + Sync {
566 async fn list_worktrees(&self) -> Result<Vec<WorkspaceGitWorktree>>;
567 async fn create_worktree(
568 &self,
569 request: WorkspaceGitCreateWorktreeRequest,
570 ) -> Result<WorkspaceGitWorktreeMutation>;
571 async fn remove_worktree(
572 &self,
573 request: WorkspaceGitRemoveWorktreeRequest,
574 ) -> Result<WorkspaceGitWorktreeMutation>;
575}
576
577#[cfg(test)]
578#[path = "tests.rs"]
579mod tests;