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 use local_access::LocalWorkspaceAccessPolicy;
38pub use manifest::{
39 scan_workspace_files, LocalWorkspaceFile, LocalWorkspaceFileStatus, LocalWorkspaceManifest,
40 LocalWorkspaceManifestSnapshot, ManifestWorkspaceBackend, RecentWorkspaceFile,
41 WorkspaceFileChange, WorkspaceFileChangeKind,
42};
43pub use path::VirtualPathResolver;
44use path::{
45 default_path_input, has_windows_path_prefix, normalize_relative_path, pathbuf_to_workspace_path,
46};
47pub(crate) use path::{escape_control_chars_for_display, validate_relative_pattern};
48pub use remote_git::{RemoteGitBackend, RemoteGitBackendConfig, RemoteGitConflict};
49pub use retrieval::{
50 ChunkCatalogLimits, ChunkCatalogSnapshot, ChunkingConfig, CustomWorkspaceChunkingStrategy,
51 FixedWindowChunkingOptions, LexicalSearchHit, LexicalSearchRequest, LexicalSearchResult,
52 RecursiveChunkingOptions, WorkspaceChunk, WorkspaceChunkCatalog, WorkspaceChunkId,
53 WorkspaceChunkRange, WorkspaceChunkingError, WorkspaceChunkingInput, WorkspaceChunkingStrategy,
54 WorkspaceEligibilityPolicy, WorkspaceEmbeddingBatchMetrics, WorkspaceHybridChannelRank,
55 WorkspaceHybridChannelStatus, WorkspaceHybridFallbackReason, WorkspaceHybridSearchHit,
56 WorkspaceHybridSearchRequest, WorkspaceHybridSearchResult, WorkspaceIndexError,
57 WorkspaceLexicalEngine, WorkspacePersistentIndex, WorkspacePersistentIndexPhase,
58 WorkspacePersistentIndexStatus, WorkspaceRerankAlgorithm, WorkspaceRerankFallbackReason,
59 WorkspaceRerankMode, WorkspaceRerankOptions, WorkspaceRerankStatus, WorkspaceRetrievalChannel,
60 WorkspaceRetrievalError, WorkspaceRetrievalOptions, WorkspaceRetrievalPhase,
61 WorkspaceRetrievalResult, WorkspaceRetrievalRuntime, WorkspaceRetrievalStatus,
62 WorkspaceSemanticFallbackReason, WorkspaceSemanticIndexLimits, WorkspaceSemanticSearchHit,
63 WorkspaceSemanticSearchRequest, WorkspaceSemanticSearchResult,
64};
65#[cfg(feature = "s3")]
66pub use s3::{S3BackendConfig, S3WorkspaceBackend};
67pub use services::{WorkspaceServices, WorkspaceServicesBuilder};
68pub use source_snapshot::{
69 workspace_content_digest, WorkspaceSourceSnapshotError, WorkspaceSourceSnapshotV1,
70 WORKSPACE_SOURCE_CONTENT_DOMAIN_V1, WORKSPACE_SOURCE_SNAPSHOT_DIGEST_DOMAIN_V1,
71 WORKSPACE_SOURCE_SNAPSHOT_SCHEMA_V1,
72};
73
74use anyhow::Result;
75use async_trait::async_trait;
76use std::collections::HashMap;
77use std::sync::Arc;
78
79#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct WorkspaceRef {
82 pub id: String,
84 pub display_root: String,
86}
87
88impl WorkspaceRef {
89 pub fn new(id: impl Into<String>, display_root: impl Into<String>) -> Self {
90 Self {
91 id: id.into(),
92 display_root: display_root.into(),
93 }
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Hash)]
99pub struct WorkspacePath {
100 inner: String,
101}
102
103impl WorkspacePath {
104 pub fn root() -> Self {
105 Self {
106 inner: ".".to_string(),
107 }
108 }
109
110 pub fn from_normalized(path: impl Into<String>) -> Self {
111 let path = path.into();
112 let path = path.trim_matches('/');
113 if path.is_empty() || path == "." {
114 Self::root()
115 } else {
116 Self {
117 inner: path.replace('\\', "/"),
118 }
119 }
120 }
121
122 pub fn as_str(&self) -> &str {
123 &self.inner
124 }
125
126 pub fn is_root(&self) -> bool {
127 self.inner == "."
128 }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub struct WorkspaceCapabilities {
138 pub read: bool,
139 pub write: bool,
140 pub exec: bool,
141 pub search: bool,
142 pub git: bool,
143 pub code_intelligence: bool,
144}
145
146impl WorkspaceCapabilities {
147 pub fn local_default() -> Self {
148 Self {
149 read: true,
150 write: true,
151 exec: true,
152 search: true,
153 git: true,
154 code_intelligence: false,
155 }
156 }
157
158 pub fn read_write() -> Self {
159 Self {
160 read: true,
161 write: true,
162 exec: false,
163 search: false,
164 git: false,
165 code_intelligence: false,
166 }
167 }
168}
169
170impl Default for WorkspaceCapabilities {
171 fn default() -> Self {
172 Self::read_write()
173 }
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub enum WorkspaceFileType {
179 File,
180 Directory,
181 Symlink,
182 Unknown,
183}
184
185impl WorkspaceFileType {
186 pub fn as_tool_kind(self) -> &'static str {
187 match self {
188 Self::File => "file",
189 Self::Directory => "dir",
190 Self::Symlink => "link",
191 Self::Unknown => "unknown",
192 }
193 }
194}
195
196#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct WorkspaceDirEntry {
199 pub name: String,
200 pub kind: WorkspaceFileType,
201 pub size: u64,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct WorkspaceWriteOutcome {
207 pub bytes: usize,
208 pub lines: usize,
209}
210
211#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct WorkspaceGlobRequest {
214 pub base: WorkspacePath,
215 pub pattern: String,
216}
217
218#[derive(Debug, Clone, PartialEq, Eq)]
220pub struct WorkspaceGlobResult {
221 pub matches: Vec<WorkspacePath>,
222}
223
224#[derive(Debug, Clone, PartialEq, Eq)]
226pub struct WorkspaceGrepRequest {
227 pub base: WorkspacePath,
228 pub pattern: String,
229 pub glob: Option<String>,
230 pub context_lines: usize,
231 pub case_insensitive: bool,
232 pub max_output_size: usize,
236}
237
238#[derive(Debug, Clone, PartialEq, Eq)]
240pub struct WorkspaceGrepResult {
241 pub output: String,
242 pub match_count: usize,
243 pub file_count: usize,
244 pub truncated: bool,
245}
246
247#[derive(Debug, Clone, PartialEq, Eq)]
249pub struct WorkspaceGrepOutcome {
250 pub result: WorkspaceGrepResult,
251 pub matched_paths: Option<Vec<WorkspacePath>>,
254}
255
256#[derive(Debug, Clone, PartialEq, Eq)]
258pub struct WorkspaceGitStatus {
259 pub branch: String,
260 pub commit: String,
261 pub is_worktree: bool,
262 pub is_dirty: bool,
263 pub dirty_count: usize,
264}
265
266#[derive(Debug, Clone, PartialEq, Eq)]
268pub struct WorkspaceGitCommit {
269 pub id: String,
270 pub message: String,
271 pub author: String,
272 pub date: String,
273}
274
275#[derive(Debug, Clone, PartialEq, Eq)]
277pub struct WorkspaceGitBranch {
278 pub name: String,
279 pub is_current: bool,
280}
281
282#[derive(Debug, Clone, PartialEq, Eq)]
284pub struct WorkspaceGitCreateBranchRequest {
285 pub name: String,
286 pub base: String,
287}
288
289#[derive(Debug, Clone, PartialEq, Eq)]
291pub struct WorkspaceGitCheckoutRequest {
292 pub refspec: String,
293 pub force: bool,
294}
295
296#[derive(Debug, Clone, PartialEq, Eq)]
298pub struct WorkspaceGitCheckoutOutput {
299 pub stdout: String,
300}
301
302#[derive(Debug, Clone, PartialEq, Eq)]
304pub struct WorkspaceGitDiffRequest {
305 pub target: Option<String>,
306}
307
308#[derive(Debug, Clone, PartialEq, Eq)]
310pub struct WorkspaceGitStash {
311 pub index: usize,
312 pub message: String,
313}
314
315#[derive(Debug, Clone, PartialEq, Eq)]
317pub struct WorkspaceGitStashRequest {
318 pub message: Option<String>,
319 pub include_untracked: bool,
320}
321
322#[derive(Debug, Clone, PartialEq, Eq)]
324pub struct WorkspaceGitRemote {
325 pub name: String,
326 pub url: String,
327 pub direction: String,
328}
329
330#[derive(Debug, Clone, PartialEq, Eq)]
332pub struct WorkspaceGitWorktree {
333 pub path: String,
334 pub branch: String,
335 pub is_bare: bool,
336 pub is_detached: bool,
337}
338
339#[derive(Debug, Clone, PartialEq, Eq)]
341pub struct WorkspaceGitCreateWorktreeRequest {
342 pub branch: String,
343 pub path: Option<String>,
344 pub new_branch: bool,
345}
346
347#[derive(Debug, Clone, PartialEq, Eq)]
349pub struct WorkspaceGitRemoveWorktreeRequest {
350 pub path: String,
351 pub force: bool,
352}
353
354#[derive(Debug, Clone, PartialEq, Eq)]
356pub struct WorkspaceGitWorktreeMutation {
357 pub path: String,
358 pub branch: Option<String>,
359}
360
361#[async_trait]
367pub trait CommandOutputObserver: Send + Sync {
368 async fn on_output_delta(&self, delta: &str);
369
370 async fn on_output_complete(&self, _summary: &CommandOutputSummary) {}
376}
377
378#[derive(Debug, Clone, Copy, PartialEq, Eq)]
380pub struct CommandOutputSummary {
381 pub total_bytes: usize,
383 pub captured_bytes: usize,
385 pub truncated: bool,
387 pub timed_out: bool,
389}
390
391#[derive(Clone)]
393pub struct CommandRequest {
394 pub command: String,
395 pub timeout_ms: u64,
396 pub output_observer: Option<Arc<dyn CommandOutputObserver>>,
397 pub env: Option<Arc<HashMap<String, String>>>,
398}
399
400impl std::fmt::Debug for CommandRequest {
401 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402 f.debug_struct("CommandRequest")
403 .field("command", &self.command)
404 .field("timeout_ms", &self.timeout_ms)
405 .field("output_observer", &self.output_observer.is_some())
406 .field("env", &self.env.as_ref().map(|env| env.len()))
407 .finish()
408 }
409}
410
411#[derive(Debug, Clone, PartialEq, Eq)]
413pub struct CommandOutput {
414 pub output: String,
415 pub exit_code: i32,
416 pub timed_out: bool,
417}
418
419pub trait WorkspacePathResolver: Send + Sync {
421 fn normalize(&self, input: &str) -> Result<WorkspacePath>;
422}
423
424#[async_trait]
434pub trait WorkspaceFileSystem: Send + Sync {
435 async fn read_text(&self, path: &WorkspacePath) -> WorkspaceResult<String>;
436 async fn write_text(
437 &self,
438 path: &WorkspacePath,
439 content: &str,
440 ) -> WorkspaceResult<WorkspaceWriteOutcome>;
441 async fn list_dir(&self, path: &WorkspacePath) -> WorkspaceResult<Vec<WorkspaceDirEntry>>;
442}
443
444#[derive(Debug, Clone, PartialEq, Eq)]
446pub struct WorkspaceTextRange {
447 pub lines: Vec<String>,
448 pub next_offset: Option<usize>,
449 pub eof: bool,
450 pub total_lines: Option<usize>,
452}
453
454#[async_trait]
457pub trait WorkspaceTextReader: Send + Sync {
458 async fn read_text_range(
459 &self,
460 path: &WorkspacePath,
461 offset: usize,
462 limit: usize,
463 ) -> WorkspaceResult<WorkspaceTextRange>;
464}
465
466#[derive(Debug, Clone, thiserror::Error)]
473#[error(
474 "version conflict on {path}: expected version {expected:?}, found {actual:?} (file modified by another writer; re-read and retry)"
475)]
476pub struct WorkspaceVersionConflict {
477 pub path: String,
478 pub expected: String,
479 pub actual: Option<String>,
482}
483
484#[async_trait]
496pub trait WorkspaceFileSystemExt: Send + Sync {
497 async fn read_text_with_version(
502 &self,
503 path: &WorkspacePath,
504 ) -> WorkspaceResult<(String, String)>;
505
506 async fn write_text_if_version(
512 &self,
513 path: &WorkspacePath,
514 content: &str,
515 expected_version: &str,
516 ) -> WorkspaceResult<WorkspaceWriteOutcome>;
517}
518
519#[async_trait]
521pub trait WorkspaceCommandRunner: Send + Sync {
522 async fn exec(&self, request: CommandRequest) -> Result<CommandOutput>;
523}
524
525#[async_trait]
527pub trait WorkspaceSearch: Send + Sync {
528 async fn glob(&self, request: WorkspaceGlobRequest) -> Result<WorkspaceGlobResult>;
529 async fn grep(&self, request: WorkspaceGrepRequest) -> Result<WorkspaceGrepResult>;
530
531 async fn grep_with_sources(
536 &self,
537 request: WorkspaceGrepRequest,
538 ) -> Result<WorkspaceGrepOutcome> {
539 let result = self.grep(request).await?;
540 Ok(WorkspaceGrepOutcome {
541 result,
542 matched_paths: None,
543 })
544 }
545}
546
547#[async_trait]
553pub trait WorkspaceGit: Send + Sync {
554 async fn is_repository(&self) -> Result<bool>;
555 async fn status(&self) -> Result<WorkspaceGitStatus>;
556 async fn log(&self, max_count: usize) -> Result<Vec<WorkspaceGitCommit>>;
557 async fn list_branches(&self) -> Result<Vec<WorkspaceGitBranch>>;
558 async fn create_branch(&self, request: WorkspaceGitCreateBranchRequest) -> Result<()>;
559 async fn checkout(
560 &self,
561 request: WorkspaceGitCheckoutRequest,
562 ) -> Result<WorkspaceGitCheckoutOutput>;
563 async fn diff(&self, request: WorkspaceGitDiffRequest) -> Result<String>;
564 async fn list_remotes(&self) -> Result<Vec<WorkspaceGitRemote>>;
565}
566
567#[async_trait]
572pub trait WorkspaceGitStashProvider: Send + Sync {
573 async fn list_stashes(&self) -> Result<Vec<WorkspaceGitStash>>;
574 async fn stash(&self, request: WorkspaceGitStashRequest) -> Result<()>;
575}
576
577#[async_trait]
582pub trait WorkspaceGitWorktreeProvider: Send + Sync {
583 async fn list_worktrees(&self) -> Result<Vec<WorkspaceGitWorktree>>;
584 async fn create_worktree(
585 &self,
586 request: WorkspaceGitCreateWorktreeRequest,
587 ) -> Result<WorkspaceGitWorktreeMutation>;
588 async fn remove_worktree(
589 &self,
590 request: WorkspaceGitRemoveWorktreeRequest,
591 ) -> Result<WorkspaceGitWorktreeMutation>;
592}
593
594#[cfg(test)]
595#[path = "tests.rs"]
596mod tests;