1#[cfg(test)]
11pub(crate) mod conformance;
12mod error;
13mod grep_candidates;
14mod local;
15mod local_access;
16mod manifest;
17mod path;
18mod remote_git;
19pub(crate) mod retrieval;
20#[cfg(feature = "s3")]
21mod s3;
22mod services;
23mod source_egress;
24pub mod source_snapshot;
25
26pub use error::{WorkspaceError, WorkspaceResult};
27pub use grep_candidates::{
28 GrepCandidateIndex, GrepCandidateSelection, SharedGrepCandidateIndex,
29 UnconstrainedGrepCandidateIndex,
30};
31#[cfg(feature = "grep-trigram")]
32pub use grep_candidates::{
33 TrigramGrepCandidateIndex, TrigramGrepCandidateIndexError, AUTO_GREP_TRIGRAM_MAX_FILES,
34 GREP_TRIGRAM_INDEX_RELATIVE_DIR,
35};
36pub use local::LocalWorkspaceBackend;
37pub(crate) use local_access::LocalWorkspaceAccessBoundary;
38
39pub(crate) trait DirectWriteGuard: Send + Sync {
44 fn refuse_direct_write(&self, path: &WorkspacePath) -> Result<()>;
45}
46pub use local_access::LocalWorkspaceAccessPolicy;
47pub use manifest::{
48 scan_workspace_files, LocalWorkspaceFile, LocalWorkspaceFileStatus, LocalWorkspaceManifest,
49 LocalWorkspaceManifestSnapshot, ManifestWorkspaceBackend, RecentWorkspaceFile,
50 WorkspaceFileChange, WorkspaceFileChangeKind,
51};
52pub use path::VirtualPathResolver;
53use path::{
54 default_path_input, has_windows_path_prefix, normalize_relative_path, pathbuf_to_workspace_path,
55};
56pub(crate) use path::{escape_control_chars_for_display, validate_relative_pattern};
57pub use remote_git::{RemoteGitBackend, RemoteGitBackendConfig, RemoteGitConflict};
58pub use retrieval::{
59 ChunkCatalogLimits, ChunkCatalogSnapshot, ChunkingConfig, CustomWorkspaceChunkingStrategy,
60 FixedWindowChunkingOptions, LexicalSearchHit, LexicalSearchRequest, LexicalSearchResult,
61 RecursiveChunkingOptions, WorkspaceChunk, WorkspaceChunkCatalog, WorkspaceChunkId,
62 WorkspaceChunkRange, WorkspaceChunkingError, WorkspaceChunkingInput, WorkspaceChunkingStrategy,
63 WorkspaceEligibilityPolicy, WorkspaceEmbeddingBatchMetrics, WorkspaceHybridChannelRank,
64 WorkspaceHybridChannelStatus, WorkspaceHybridFallbackReason, WorkspaceHybridSearchHit,
65 WorkspaceHybridSearchRequest, WorkspaceHybridSearchResult, WorkspaceIndexError,
66 WorkspaceLexicalEngine, WorkspacePersistentIndex, WorkspacePersistentIndexPhase,
67 WorkspacePersistentIndexStatus, WorkspaceRerankAlgorithm, WorkspaceRerankFallbackReason,
68 WorkspaceRerankMode, WorkspaceRerankOptions, WorkspaceRerankStatus, WorkspaceRetrievalChannel,
69 WorkspaceRetrievalError, WorkspaceRetrievalOptions, WorkspaceRetrievalPhase,
70 WorkspaceRetrievalResult, WorkspaceRetrievalRuntime, WorkspaceRetrievalStatus,
71 WorkspaceSemanticFallbackReason, WorkspaceSemanticIndexLimits, WorkspaceSemanticSearchHit,
72 WorkspaceSemanticSearchRequest, WorkspaceSemanticSearchResult,
73};
74#[cfg(feature = "s3")]
75pub use s3::{S3BackendConfig, S3WorkspaceBackend};
76pub use services::{WorkspaceServices, WorkspaceServicesBuilder};
77pub use source_snapshot::{
78 workspace_content_digest, WorkspaceSourceSnapshotError, WorkspaceSourceSnapshotV1,
79 WORKSPACE_SOURCE_CONTENT_DOMAIN_V1, WORKSPACE_SOURCE_SNAPSHOT_DIGEST_DOMAIN_V1,
80 WORKSPACE_SOURCE_SNAPSHOT_SCHEMA_V1,
81};
82
83use anyhow::Result;
84use async_trait::async_trait;
85use std::collections::HashMap;
86use std::sync::Arc;
87
88#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct WorkspaceRef {
91 pub id: String,
93 pub display_root: String,
95}
96
97impl WorkspaceRef {
98 pub fn new(id: impl Into<String>, display_root: impl Into<String>) -> Self {
99 Self {
100 id: id.into(),
101 display_root: display_root.into(),
102 }
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Hash)]
108pub struct WorkspacePath {
109 inner: String,
110}
111
112impl WorkspacePath {
113 pub fn root() -> Self {
114 Self {
115 inner: ".".to_string(),
116 }
117 }
118
119 pub fn from_normalized(path: impl Into<String>) -> Self {
120 let path = path.into();
121 let path = path.trim_matches('/');
122 if path.is_empty() || path == "." {
123 Self::root()
124 } else {
125 Self {
126 inner: path.replace('\\', "/"),
127 }
128 }
129 }
130
131 pub fn as_str(&self) -> &str {
132 &self.inner
133 }
134
135 pub fn is_root(&self) -> bool {
136 self.inner == "."
137 }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub struct WorkspaceCapabilities {
147 pub read: bool,
148 pub write: bool,
149 pub exec: bool,
150 pub search: bool,
151 pub git: bool,
152 pub code_intelligence: bool,
153}
154
155impl WorkspaceCapabilities {
156 pub fn local_default() -> Self {
157 Self {
158 read: true,
159 write: true,
160 exec: true,
161 search: true,
162 git: true,
163 code_intelligence: false,
164 }
165 }
166
167 pub fn read_write() -> Self {
168 Self {
169 read: true,
170 write: true,
171 exec: false,
172 search: false,
173 git: false,
174 code_intelligence: false,
175 }
176 }
177}
178
179impl Default for WorkspaceCapabilities {
180 fn default() -> Self {
181 Self::read_write()
182 }
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187pub enum WorkspaceFileType {
188 File,
189 Directory,
190 Symlink,
191 Unknown,
192}
193
194impl WorkspaceFileType {
195 pub fn as_tool_kind(self) -> &'static str {
196 match self {
197 Self::File => "file",
198 Self::Directory => "dir",
199 Self::Symlink => "link",
200 Self::Unknown => "unknown",
201 }
202 }
203}
204
205#[derive(Debug, Clone, PartialEq, Eq)]
207pub struct WorkspaceDirEntry {
208 pub name: String,
209 pub kind: WorkspaceFileType,
210 pub size: u64,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
215pub struct WorkspaceWriteOutcome {
216 pub bytes: usize,
217 pub lines: usize,
218}
219
220#[derive(Debug, Clone, PartialEq, Eq)]
222pub struct WorkspaceGlobRequest {
223 pub base: WorkspacePath,
224 pub pattern: String,
225}
226
227#[derive(Debug, Clone, PartialEq, Eq)]
229pub struct WorkspaceGlobResult {
230 pub matches: Vec<WorkspacePath>,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq)]
235pub struct WorkspaceGrepRequest {
236 pub base: WorkspacePath,
237 pub pattern: String,
238 pub glob: Option<String>,
239 pub context_lines: usize,
240 pub case_insensitive: bool,
241 pub max_output_size: usize,
245}
246
247#[derive(Debug, Clone, PartialEq, Eq)]
249pub struct WorkspaceGrepResult {
250 pub output: String,
251 pub match_count: usize,
252 pub file_count: usize,
253 pub truncated: bool,
254}
255
256#[derive(Debug, Clone, PartialEq, Eq)]
258pub struct WorkspaceGrepOutcome {
259 pub result: WorkspaceGrepResult,
260 pub matched_paths: Option<Vec<WorkspacePath>>,
263}
264
265#[derive(Debug, Clone, PartialEq, Eq)]
267pub struct WorkspaceGitStatus {
268 pub branch: String,
269 pub commit: String,
270 pub is_worktree: bool,
271 pub is_dirty: bool,
272 pub dirty_count: usize,
273}
274
275#[derive(Debug, Clone, PartialEq, Eq)]
277pub struct WorkspaceGitCommit {
278 pub id: String,
279 pub message: String,
280 pub author: String,
281 pub date: String,
282}
283
284#[derive(Debug, Clone, PartialEq, Eq)]
286pub struct WorkspaceGitBranch {
287 pub name: String,
288 pub is_current: bool,
289}
290
291#[derive(Debug, Clone, PartialEq, Eq)]
293pub struct WorkspaceGitCreateBranchRequest {
294 pub name: String,
295 pub base: String,
296}
297
298#[derive(Debug, Clone, PartialEq, Eq)]
300pub struct WorkspaceGitCheckoutRequest {
301 pub refspec: String,
302 pub force: bool,
303}
304
305#[derive(Debug, Clone, PartialEq, Eq)]
307pub struct WorkspaceGitCheckoutOutput {
308 pub stdout: String,
309}
310
311#[derive(Debug, Clone, PartialEq, Eq)]
313pub struct WorkspaceGitDiffRequest {
314 pub target: Option<String>,
315}
316
317#[derive(Debug, Clone, PartialEq, Eq)]
319pub struct WorkspaceGitStash {
320 pub index: usize,
321 pub message: String,
322}
323
324#[derive(Debug, Clone, PartialEq, Eq)]
326pub struct WorkspaceGitStashRequest {
327 pub message: Option<String>,
328 pub include_untracked: bool,
329}
330
331#[derive(Debug, Clone, PartialEq, Eq)]
333pub struct WorkspaceGitRemote {
334 pub name: String,
335 pub url: String,
336 pub direction: String,
337}
338
339#[derive(Debug, Clone, PartialEq, Eq)]
341pub struct WorkspaceGitWorktree {
342 pub path: String,
343 pub branch: String,
344 pub is_bare: bool,
345 pub is_detached: bool,
346}
347
348#[derive(Debug, Clone, PartialEq, Eq)]
350pub struct WorkspaceGitCreateWorktreeRequest {
351 pub branch: String,
352 pub path: Option<String>,
353 pub new_branch: bool,
354}
355
356#[derive(Debug, Clone, PartialEq, Eq)]
358pub struct WorkspaceGitRemoveWorktreeRequest {
359 pub path: String,
360 pub force: bool,
361}
362
363#[derive(Debug, Clone, PartialEq, Eq)]
365pub struct WorkspaceGitWorktreeMutation {
366 pub path: String,
367 pub branch: Option<String>,
368}
369
370#[async_trait]
376pub trait CommandOutputObserver: Send + Sync {
377 async fn on_output_delta(&self, delta: &str);
378
379 async fn on_output_complete(&self, _summary: &CommandOutputSummary) {}
385}
386
387#[derive(Debug, Clone, Copy, PartialEq, Eq)]
389pub struct CommandOutputSummary {
390 pub total_bytes: usize,
392 pub captured_bytes: usize,
394 pub truncated: bool,
396 pub timed_out: bool,
398}
399
400#[derive(Clone)]
402pub struct CommandRequest {
403 pub command: String,
404 pub timeout_ms: u64,
405 pub output_observer: Option<Arc<dyn CommandOutputObserver>>,
406 pub env: Option<Arc<HashMap<String, String>>>,
407}
408
409impl std::fmt::Debug for CommandRequest {
410 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411 f.debug_struct("CommandRequest")
412 .field("command", &self.command)
413 .field("timeout_ms", &self.timeout_ms)
414 .field("output_observer", &self.output_observer.is_some())
415 .field("env", &self.env.as_ref().map(|env| env.len()))
416 .finish()
417 }
418}
419
420#[derive(Debug, Clone, PartialEq, Eq)]
422pub struct CommandOutput {
423 pub output: String,
424 pub exit_code: i32,
425 pub timed_out: bool,
426}
427
428pub trait WorkspacePathResolver: Send + Sync {
430 fn normalize(&self, input: &str) -> Result<WorkspacePath>;
431}
432
433#[async_trait]
443pub trait WorkspaceFileSystem: Send + Sync {
444 async fn read_text(&self, path: &WorkspacePath) -> WorkspaceResult<String>;
445 async fn write_text(
446 &self,
447 path: &WorkspacePath,
448 content: &str,
449 ) -> WorkspaceResult<WorkspaceWriteOutcome>;
450 async fn list_dir(&self, path: &WorkspacePath) -> WorkspaceResult<Vec<WorkspaceDirEntry>>;
451}
452
453#[derive(Debug, Clone, PartialEq, Eq)]
455pub struct WorkspaceTextRange {
456 pub lines: Vec<String>,
457 pub next_offset: Option<usize>,
458 pub eof: bool,
459 pub total_lines: Option<usize>,
461}
462
463#[async_trait]
466pub trait WorkspaceTextReader: Send + Sync {
467 async fn read_text_range(
468 &self,
469 path: &WorkspacePath,
470 offset: usize,
471 limit: usize,
472 ) -> WorkspaceResult<WorkspaceTextRange>;
473}
474
475#[derive(Debug, Clone, thiserror::Error)]
482#[error(
483 "version conflict on {path}: expected version {expected:?}, found {actual:?} (file modified by another writer; re-read and retry)"
484)]
485pub struct WorkspaceVersionConflict {
486 pub path: String,
487 pub expected: String,
488 pub actual: Option<String>,
491}
492
493#[async_trait]
505pub trait WorkspaceFileSystemExt: Send + Sync {
506 async fn read_text_with_version(
511 &self,
512 path: &WorkspacePath,
513 ) -> WorkspaceResult<(String, String)>;
514
515 async fn write_text_if_version(
521 &self,
522 path: &WorkspacePath,
523 content: &str,
524 expected_version: &str,
525 ) -> WorkspaceResult<WorkspaceWriteOutcome>;
526}
527
528#[async_trait]
530pub trait WorkspaceCommandRunner: Send + Sync {
531 async fn exec(&self, request: CommandRequest) -> Result<CommandOutput>;
532}
533
534#[async_trait]
536pub trait WorkspaceSearch: Send + Sync {
537 async fn glob(&self, request: WorkspaceGlobRequest) -> Result<WorkspaceGlobResult>;
538 async fn grep(&self, request: WorkspaceGrepRequest) -> Result<WorkspaceGrepResult>;
539
540 async fn grep_with_sources(
545 &self,
546 request: WorkspaceGrepRequest,
547 ) -> Result<WorkspaceGrepOutcome> {
548 let result = self.grep(request).await?;
549 Ok(WorkspaceGrepOutcome {
550 result,
551 matched_paths: None,
552 })
553 }
554}
555
556#[async_trait]
562pub trait WorkspaceGit: Send + Sync {
563 async fn is_repository(&self) -> Result<bool>;
564 async fn status(&self) -> Result<WorkspaceGitStatus>;
565 async fn log(&self, max_count: usize) -> Result<Vec<WorkspaceGitCommit>>;
566 async fn list_branches(&self) -> Result<Vec<WorkspaceGitBranch>>;
567 async fn create_branch(&self, request: WorkspaceGitCreateBranchRequest) -> Result<()>;
568 async fn checkout(
569 &self,
570 request: WorkspaceGitCheckoutRequest,
571 ) -> Result<WorkspaceGitCheckoutOutput>;
572 async fn diff(&self, request: WorkspaceGitDiffRequest) -> Result<String>;
573 async fn list_remotes(&self) -> Result<Vec<WorkspaceGitRemote>>;
574}
575
576#[async_trait]
581pub trait WorkspaceGitStashProvider: Send + Sync {
582 async fn list_stashes(&self) -> Result<Vec<WorkspaceGitStash>>;
583 async fn stash(&self, request: WorkspaceGitStashRequest) -> Result<()>;
584}
585
586#[async_trait]
591pub trait WorkspaceGitWorktreeProvider: Send + Sync {
592 async fn list_worktrees(&self) -> Result<Vec<WorkspaceGitWorktree>>;
593 async fn create_worktree(
594 &self,
595 request: WorkspaceGitCreateWorktreeRequest,
596 ) -> Result<WorkspaceGitWorktreeMutation>;
597 async fn remove_worktree(
598 &self,
599 request: WorkspaceGitRemoveWorktreeRequest,
600 ) -> Result<WorkspaceGitWorktreeMutation>;
601}
602
603#[cfg(test)]
604#[path = "tests.rs"]
605mod tests;