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;
23pub mod source_snapshot;
24
25pub use error::{WorkspaceError, WorkspaceResult};
26pub use local::LocalWorkspaceBackend;
27pub use local_access::LocalWorkspaceAccessPolicy;
28pub use manifest::{
29 scan_workspace_files, LocalWorkspaceFile, LocalWorkspaceFileStatus, LocalWorkspaceManifest,
30 LocalWorkspaceManifestSnapshot, ManifestWorkspaceBackend, RecentWorkspaceFile,
31 WorkspaceFileChange, WorkspaceFileChangeKind,
32};
33pub use path::VirtualPathResolver;
34use path::{
35 default_path_input, has_windows_path_prefix, normalize_relative_path, pathbuf_to_workspace_path,
36};
37pub(crate) use path::{escape_control_chars_for_display, validate_relative_pattern};
38pub use remote_git::{RemoteGitBackend, RemoteGitBackendConfig, RemoteGitConflict};
39pub use retrieval::{
40 ChunkCatalogLimits, ChunkCatalogSnapshot, ChunkingConfig, CustomWorkspaceChunkingStrategy,
41 FixedWindowChunkingOptions, LexicalSearchHit, LexicalSearchRequest, LexicalSearchResult,
42 RecursiveChunkingOptions, WorkspaceChunk, WorkspaceChunkCatalog, WorkspaceChunkId,
43 WorkspaceChunkRange, WorkspaceChunkingError, WorkspaceChunkingInput, WorkspaceChunkingStrategy,
44 WorkspaceEligibilityPolicy, WorkspaceEmbeddingBatchMetrics, WorkspaceHybridChannelRank,
45 WorkspaceHybridChannelStatus, WorkspaceHybridFallbackReason, WorkspaceHybridSearchHit,
46 WorkspaceHybridSearchRequest, WorkspaceHybridSearchResult, WorkspaceIndexError,
47 WorkspaceLexicalEngine, WorkspacePersistentIndex, WorkspacePersistentIndexPhase,
48 WorkspacePersistentIndexStatus, WorkspaceRerankAlgorithm, WorkspaceRerankFallbackReason,
49 WorkspaceRerankMode, WorkspaceRerankOptions, WorkspaceRerankStatus, WorkspaceRetrievalChannel,
50 WorkspaceRetrievalError, WorkspaceRetrievalOptions, WorkspaceRetrievalPhase,
51 WorkspaceRetrievalResult, WorkspaceRetrievalRuntime, WorkspaceRetrievalStatus,
52 WorkspaceSemanticFallbackReason, WorkspaceSemanticIndexLimits, WorkspaceSemanticSearchHit,
53 WorkspaceSemanticSearchRequest, WorkspaceSemanticSearchResult,
54};
55#[cfg(feature = "s3")]
56pub use s3::{S3BackendConfig, S3WorkspaceBackend};
57pub use services::{WorkspaceServices, WorkspaceServicesBuilder};
58pub use source_snapshot::{
59 workspace_content_digest, WorkspaceSourceSnapshotError, WorkspaceSourceSnapshotV1,
60 WORKSPACE_SOURCE_CONTENT_DOMAIN_V1, WORKSPACE_SOURCE_SNAPSHOT_DIGEST_DOMAIN_V1,
61 WORKSPACE_SOURCE_SNAPSHOT_SCHEMA_V1,
62};
63
64use anyhow::Result;
65use async_trait::async_trait;
66use std::collections::HashMap;
67use std::sync::Arc;
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct WorkspaceRef {
72 pub id: String,
74 pub display_root: String,
76}
77
78impl WorkspaceRef {
79 pub fn new(id: impl Into<String>, display_root: impl Into<String>) -> Self {
80 Self {
81 id: id.into(),
82 display_root: display_root.into(),
83 }
84 }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Hash)]
89pub struct WorkspacePath {
90 inner: String,
91}
92
93impl WorkspacePath {
94 pub fn root() -> Self {
95 Self {
96 inner: ".".to_string(),
97 }
98 }
99
100 pub fn from_normalized(path: impl Into<String>) -> Self {
101 let path = path.into();
102 let path = path.trim_matches('/');
103 if path.is_empty() || path == "." {
104 Self::root()
105 } else {
106 Self {
107 inner: path.replace('\\', "/"),
108 }
109 }
110 }
111
112 pub fn as_str(&self) -> &str {
113 &self.inner
114 }
115
116 pub fn is_root(&self) -> bool {
117 self.inner == "."
118 }
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub struct WorkspaceCapabilities {
128 pub read: bool,
129 pub write: bool,
130 pub exec: bool,
131 pub search: bool,
132 pub git: bool,
133 pub code_intelligence: bool,
134}
135
136impl WorkspaceCapabilities {
137 pub fn local_default() -> Self {
138 Self {
139 read: true,
140 write: true,
141 exec: true,
142 search: true,
143 git: true,
144 code_intelligence: false,
145 }
146 }
147
148 pub fn read_write() -> Self {
149 Self {
150 read: true,
151 write: true,
152 exec: false,
153 search: false,
154 git: false,
155 code_intelligence: false,
156 }
157 }
158}
159
160impl Default for WorkspaceCapabilities {
161 fn default() -> Self {
162 Self::read_write()
163 }
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub enum WorkspaceFileType {
169 File,
170 Directory,
171 Symlink,
172 Unknown,
173}
174
175impl WorkspaceFileType {
176 pub fn as_tool_kind(self) -> &'static str {
177 match self {
178 Self::File => "file",
179 Self::Directory => "dir",
180 Self::Symlink => "link",
181 Self::Unknown => "unknown",
182 }
183 }
184}
185
186#[derive(Debug, Clone, PartialEq, Eq)]
188pub struct WorkspaceDirEntry {
189 pub name: String,
190 pub kind: WorkspaceFileType,
191 pub size: u64,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct WorkspaceWriteOutcome {
197 pub bytes: usize,
198 pub lines: usize,
199}
200
201#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct WorkspaceGlobRequest {
204 pub base: WorkspacePath,
205 pub pattern: String,
206}
207
208#[derive(Debug, Clone, PartialEq, Eq)]
210pub struct WorkspaceGlobResult {
211 pub matches: Vec<WorkspacePath>,
212}
213
214#[derive(Debug, Clone, PartialEq, Eq)]
216pub struct WorkspaceGrepRequest {
217 pub base: WorkspacePath,
218 pub pattern: String,
219 pub glob: Option<String>,
220 pub context_lines: usize,
221 pub case_insensitive: bool,
222 pub max_output_size: usize,
226}
227
228#[derive(Debug, Clone, PartialEq, Eq)]
230pub struct WorkspaceGrepResult {
231 pub output: String,
232 pub match_count: usize,
233 pub file_count: usize,
234 pub truncated: bool,
235}
236
237#[derive(Debug, Clone, PartialEq, Eq)]
239pub struct WorkspaceGrepOutcome {
240 pub result: WorkspaceGrepResult,
241 pub matched_paths: Option<Vec<WorkspacePath>>,
244}
245
246#[derive(Debug, Clone, PartialEq, Eq)]
248pub struct WorkspaceGitStatus {
249 pub branch: String,
250 pub commit: String,
251 pub is_worktree: bool,
252 pub is_dirty: bool,
253 pub dirty_count: usize,
254}
255
256#[derive(Debug, Clone, PartialEq, Eq)]
258pub struct WorkspaceGitCommit {
259 pub id: String,
260 pub message: String,
261 pub author: String,
262 pub date: String,
263}
264
265#[derive(Debug, Clone, PartialEq, Eq)]
267pub struct WorkspaceGitBranch {
268 pub name: String,
269 pub is_current: bool,
270}
271
272#[derive(Debug, Clone, PartialEq, Eq)]
274pub struct WorkspaceGitCreateBranchRequest {
275 pub name: String,
276 pub base: String,
277}
278
279#[derive(Debug, Clone, PartialEq, Eq)]
281pub struct WorkspaceGitCheckoutRequest {
282 pub refspec: String,
283 pub force: bool,
284}
285
286#[derive(Debug, Clone, PartialEq, Eq)]
288pub struct WorkspaceGitCheckoutOutput {
289 pub stdout: String,
290}
291
292#[derive(Debug, Clone, PartialEq, Eq)]
294pub struct WorkspaceGitDiffRequest {
295 pub target: Option<String>,
296}
297
298#[derive(Debug, Clone, PartialEq, Eq)]
300pub struct WorkspaceGitStash {
301 pub index: usize,
302 pub message: String,
303}
304
305#[derive(Debug, Clone, PartialEq, Eq)]
307pub struct WorkspaceGitStashRequest {
308 pub message: Option<String>,
309 pub include_untracked: bool,
310}
311
312#[derive(Debug, Clone, PartialEq, Eq)]
314pub struct WorkspaceGitRemote {
315 pub name: String,
316 pub url: String,
317 pub direction: String,
318}
319
320#[derive(Debug, Clone, PartialEq, Eq)]
322pub struct WorkspaceGitWorktree {
323 pub path: String,
324 pub branch: String,
325 pub is_bare: bool,
326 pub is_detached: bool,
327}
328
329#[derive(Debug, Clone, PartialEq, Eq)]
331pub struct WorkspaceGitCreateWorktreeRequest {
332 pub branch: String,
333 pub path: Option<String>,
334 pub new_branch: bool,
335}
336
337#[derive(Debug, Clone, PartialEq, Eq)]
339pub struct WorkspaceGitRemoveWorktreeRequest {
340 pub path: String,
341 pub force: bool,
342}
343
344#[derive(Debug, Clone, PartialEq, Eq)]
346pub struct WorkspaceGitWorktreeMutation {
347 pub path: String,
348 pub branch: Option<String>,
349}
350
351#[async_trait]
357pub trait CommandOutputObserver: Send + Sync {
358 async fn on_output_delta(&self, delta: &str);
359
360 async fn on_output_complete(&self, _summary: &CommandOutputSummary) {}
366}
367
368#[derive(Debug, Clone, Copy, PartialEq, Eq)]
370pub struct CommandOutputSummary {
371 pub total_bytes: usize,
373 pub captured_bytes: usize,
375 pub truncated: bool,
377 pub timed_out: bool,
379}
380
381#[derive(Clone)]
383pub struct CommandRequest {
384 pub command: String,
385 pub timeout_ms: u64,
386 pub output_observer: Option<Arc<dyn CommandOutputObserver>>,
387 pub env: Option<Arc<HashMap<String, String>>>,
388}
389
390impl std::fmt::Debug for CommandRequest {
391 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
392 f.debug_struct("CommandRequest")
393 .field("command", &self.command)
394 .field("timeout_ms", &self.timeout_ms)
395 .field("output_observer", &self.output_observer.is_some())
396 .field("env", &self.env.as_ref().map(|env| env.len()))
397 .finish()
398 }
399}
400
401#[derive(Debug, Clone, PartialEq, Eq)]
403pub struct CommandOutput {
404 pub output: String,
405 pub exit_code: i32,
406 pub timed_out: bool,
407}
408
409pub trait WorkspacePathResolver: Send + Sync {
411 fn normalize(&self, input: &str) -> Result<WorkspacePath>;
412}
413
414#[async_trait]
424pub trait WorkspaceFileSystem: Send + Sync {
425 async fn read_text(&self, path: &WorkspacePath) -> WorkspaceResult<String>;
426 async fn write_text(
427 &self,
428 path: &WorkspacePath,
429 content: &str,
430 ) -> WorkspaceResult<WorkspaceWriteOutcome>;
431 async fn list_dir(&self, path: &WorkspacePath) -> WorkspaceResult<Vec<WorkspaceDirEntry>>;
432}
433
434#[derive(Debug, Clone, PartialEq, Eq)]
436pub struct WorkspaceTextRange {
437 pub lines: Vec<String>,
438 pub next_offset: Option<usize>,
439 pub eof: bool,
440 pub total_lines: Option<usize>,
442}
443
444#[async_trait]
447pub trait WorkspaceTextReader: Send + Sync {
448 async fn read_text_range(
449 &self,
450 path: &WorkspacePath,
451 offset: usize,
452 limit: usize,
453 ) -> WorkspaceResult<WorkspaceTextRange>;
454}
455
456#[derive(Debug, Clone, thiserror::Error)]
463#[error(
464 "version conflict on {path}: expected version {expected:?}, found {actual:?} (file modified by another writer; re-read and retry)"
465)]
466pub struct WorkspaceVersionConflict {
467 pub path: String,
468 pub expected: String,
469 pub actual: Option<String>,
472}
473
474#[async_trait]
486pub trait WorkspaceFileSystemExt: Send + Sync {
487 async fn read_text_with_version(
492 &self,
493 path: &WorkspacePath,
494 ) -> WorkspaceResult<(String, String)>;
495
496 async fn write_text_if_version(
502 &self,
503 path: &WorkspacePath,
504 content: &str,
505 expected_version: &str,
506 ) -> WorkspaceResult<WorkspaceWriteOutcome>;
507}
508
509#[async_trait]
511pub trait WorkspaceCommandRunner: Send + Sync {
512 async fn exec(&self, request: CommandRequest) -> Result<CommandOutput>;
513}
514
515#[async_trait]
517pub trait WorkspaceSearch: Send + Sync {
518 async fn glob(&self, request: WorkspaceGlobRequest) -> Result<WorkspaceGlobResult>;
519 async fn grep(&self, request: WorkspaceGrepRequest) -> Result<WorkspaceGrepResult>;
520
521 async fn grep_with_sources(
526 &self,
527 request: WorkspaceGrepRequest,
528 ) -> Result<WorkspaceGrepOutcome> {
529 let result = self.grep(request).await?;
530 Ok(WorkspaceGrepOutcome {
531 result,
532 matched_paths: None,
533 })
534 }
535}
536
537#[async_trait]
543pub trait WorkspaceGit: Send + Sync {
544 async fn is_repository(&self) -> Result<bool>;
545 async fn status(&self) -> Result<WorkspaceGitStatus>;
546 async fn log(&self, max_count: usize) -> Result<Vec<WorkspaceGitCommit>>;
547 async fn list_branches(&self) -> Result<Vec<WorkspaceGitBranch>>;
548 async fn create_branch(&self, request: WorkspaceGitCreateBranchRequest) -> Result<()>;
549 async fn checkout(
550 &self,
551 request: WorkspaceGitCheckoutRequest,
552 ) -> Result<WorkspaceGitCheckoutOutput>;
553 async fn diff(&self, request: WorkspaceGitDiffRequest) -> Result<String>;
554 async fn list_remotes(&self) -> Result<Vec<WorkspaceGitRemote>>;
555}
556
557#[async_trait]
562pub trait WorkspaceGitStashProvider: Send + Sync {
563 async fn list_stashes(&self) -> Result<Vec<WorkspaceGitStash>>;
564 async fn stash(&self, request: WorkspaceGitStashRequest) -> Result<()>;
565}
566
567#[async_trait]
572pub trait WorkspaceGitWorktreeProvider: Send + Sync {
573 async fn list_worktrees(&self) -> Result<Vec<WorkspaceGitWorktree>>;
574 async fn create_worktree(
575 &self,
576 request: WorkspaceGitCreateWorktreeRequest,
577 ) -> Result<WorkspaceGitWorktreeMutation>;
578 async fn remove_worktree(
579 &self,
580 request: WorkspaceGitRemoveWorktreeRequest,
581 ) -> Result<WorkspaceGitWorktreeMutation>;
582}
583
584#[cfg(test)]
585#[path = "tests.rs"]
586mod tests;