Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/crates/core/src/service/snapshot/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,8 @@ impl WrappedTool {
debug!("Creating new file: file_path={}", file_path.display());
}

let file_existed_before = file_path.exists();
let operation_type = self.get_operation_type_internal(file_existed_before);
let turn_index = self.extract_turn_index(context);

let snapshot_service = snapshot_manager.get_snapshot_service();
Expand All @@ -520,7 +522,7 @@ impl WrappedTool {
self.name(),
input.clone(),
&file_path,
self.get_operation_type_internal(),
operation_type,
context.tool_call_id.clone(),
)
.await
Expand Down Expand Up @@ -577,8 +579,15 @@ impl WrappedTool {
}

/// Returns the operation type.
fn get_operation_type_internal(&self) -> OperationType {
fn get_operation_type_internal(&self, file_existed_before: bool) -> OperationType {
match self.name() {
"Write" | "write_file" => {
if file_existed_before {
OperationType::Modify
} else {
OperationType::Create
}
}
"create_file" => OperationType::Create,
"delete_file" | "Delete" => OperationType::Delete,
"rename_file" | "move_file" => OperationType::Rename,
Expand Down
82 changes: 56 additions & 26 deletions src/crates/core/src/service/snapshot/snapshot_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ impl SnapshotCore {
None
};

if let Some(before_id) = &before_snapshot_id {
if !self.snapshot_system.has_baseline(&file_path).await {
match self
if !self.snapshot_system.has_baseline(&file_path).await {
match &before_snapshot_id {
Some(before_id) => match self
.snapshot_system
.create_baseline_from_snapshot(&file_path, before_id)
.await
Expand All @@ -149,13 +149,30 @@ impl SnapshotCore {
file_path, e
);
}
},
None if operation_type == OperationType::Create => {
match self.snapshot_system.create_empty_baseline(&file_path).await {
Ok(baseline_id) => {
debug!(
"Created empty baseline snapshot for new file: file_path={:?} baseline_id={}",
file_path, baseline_id
);
}
Err(e) => {
warn!(
"Failed to create empty baseline snapshot: file_path={:?} error={}",
file_path, e
);
}
}
}
} else {
debug!(
"Baseline snapshot already exists: file_path={:?}",
file_path
);
None => {}
}
} else {
debug!(
"Baseline snapshot already exists: file_path={:?}",
file_path
);
}

let session = self
Expand Down Expand Up @@ -431,6 +448,11 @@ impl SnapshotCore {
return Err(SnapshotError::SessionNotFound(session_id.to_string()));
};

let first_op_for_file = session
.all_operations_iter()
.find(|op| op.file_path == file_path);
let file_created_in_session = matches!(first_op_for_file, Some(op) if op.before_snapshot_id.is_none());

let load_first_before = || async {
let first_before = session
.all_operations_iter()
Expand All @@ -447,31 +469,39 @@ impl SnapshotCore {
.unwrap_or_default()
};

let before = if let Some(baseline_id) = self
.snapshot_system
.get_baseline_snapshot_id(file_path)
.await
{
let before = if file_created_in_session {
debug!(
"Using baseline snapshot for diff: file_path={:?} baseline_id={}",
file_path, baseline_id
"Using empty baseline for session-created file diff: file_path={:?} session_id={}",
file_path, session_id
);
match self
String::new()
} else {
if let Some(baseline_id) = self
.snapshot_system
.get_snapshot_content(&baseline_id)
.get_baseline_snapshot_id(file_path)
.await
{
Ok(content) => content,
Err(e) => {
warn!(
"Failed to read baseline snapshot, falling back to first before snapshot: baseline_id={} error={}",
baseline_id, e
);
load_first_before().await
debug!(
"Using baseline snapshot for diff: file_path={:?} baseline_id={}",
file_path, baseline_id
);
match self
.snapshot_system
.get_snapshot_content(&baseline_id)
.await
{
Ok(content) => content,
Err(e) => {
warn!(
"Failed to read baseline snapshot, falling back to first before snapshot: baseline_id={} error={}",
baseline_id, e
);
load_first_before().await
}
}
} else {
load_first_before().await
}
} else {
load_first_before().await
};

let after = if file_path.exists() {
Expand Down
76 changes: 67 additions & 9 deletions src/crates/core/src/service/snapshot/snapshot_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,46 @@ impl BaselineCache {

Ok(baseline_id)
}

/// Creates an empty baseline for files that are first introduced during the session.
pub async fn create_empty(
&self,
file_path: &Path,
empty_content_hash: &str,
content_path: &Path,
) -> SnapshotResult<String> {
let baseline_id = format!("baseline_empty_{}", Uuid::new_v4());

if !content_path.exists() {
fs::write(content_path, [])?;
}

let baseline_metadata = FileSnapshot {
snapshot_id: baseline_id.clone(),
file_path: file_path.to_path_buf(),
content_hash: empty_content_hash.to_string(),
snapshot_type: SnapshotType::Baseline,
compressed_content: Vec::new(),
timestamp: SystemTime::now(),
metadata: FileMetadata {
size: 0,
permissions: None,
last_modified: SystemTime::now(),
encoding: "utf-8".to_string(),
},
};

let baseline_meta_path = self.baseline_dir.join(format!("{}.json", baseline_id));
let metadata_json = serde_json::to_string_pretty(&baseline_metadata)?;
fs::write(&baseline_meta_path, metadata_json)?;

{
let mut cache = self.cache.write().await;
cache.insert(file_path.to_path_buf(), Some(baseline_id.clone()));
}

Ok(baseline_id)
}
}

/// Simplified file snapshot system
Expand Down Expand Up @@ -302,11 +342,18 @@ impl FileSnapshotSystem {
let content_hash = self.calculate_content_hash(&content);

if self.dedup_enabled && self.hash_to_path.contains_key(&content_hash) {
if let Some(snapshot_id) = self.find_snapshot_by_hash(&content_hash) {
debug!(
"Found duplicate content, reusing existing snapshot: content_hash={}",
content_hash
);
return Ok(snapshot_id);
}

debug!(
"Found duplicate content, reusing existing snapshot: content_hash={}",
"Found reusable content without active snapshot metadata, creating new snapshot metadata: content_hash={}",
content_hash
);
return Ok(self.find_snapshot_by_hash(&content_hash)?);
}

let optimized_content = self.optimize_content(&content);
Expand Down Expand Up @@ -473,17 +520,13 @@ impl FileSnapshotSystem {
}

/// Finds a snapshot ID by hash.
fn find_snapshot_by_hash(&self, content_hash: &str) -> SnapshotResult<String> {
fn find_snapshot_by_hash(&self, content_hash: &str) -> Option<String> {
for (snapshot_id, snapshot) in &self.active_snapshots {
if snapshot.content_hash == content_hash {
return Ok(snapshot_id.clone());
return Some(snapshot_id.clone());
}
}

Err(SnapshotError::SnapshotNotFound(format!(
"hash: {}",
content_hash
)))
None
}

/// Loads snapshot metadata from disk (without using in-memory cache).
Expand Down Expand Up @@ -778,6 +821,21 @@ impl FileSnapshotSystem {
.await
}

/// Creates an empty baseline for files that did not exist before the session.
pub async fn create_empty_baseline(&mut self, file_path: &Path) -> SnapshotResult<String> {
let empty_content_hash = self.calculate_content_hash(&[]);
let content_path = self.get_content_path(&empty_content_hash);

if !self.hash_to_path.contains_key(&empty_content_hash) {
self.hash_to_path
.insert(empty_content_hash.clone(), content_path.clone());
}

self.baseline_cache
.create_empty(file_path, &empty_content_hash, &content_path)
.await
}

/// Checks whether the file has a baseline.
pub async fn has_baseline(&self, file_path: &Path) -> bool {
self.get_baseline_snapshot_id(file_path).await.is_some()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,10 @@ export const SessionFileModificationsBar: React.FC<SessionFileModificationsBarPr

try {
const diffData = await snapshotAPI.getOperationDiff(sessionId, filePath);
if ((diffData.originalContent || '') === (diffData.modifiedContent || '')) {
log.debug('Skipping empty session diff', { filePath, sessionId });
return;
}
const fileName = filePath.split(/[/\\]/).pop() || filePath;

window.dispatchEvent(new CustomEvent('expand-right-panel'));
Expand All @@ -251,7 +255,13 @@ export const SessionFileModificationsBar: React.FC<SessionFileModificationsBarPr
diffData.modifiedContent || '',
false,
'agent',
currentWorkspace?.rootPath
currentWorkspace?.rootPath,
undefined,
false,
{
titleKind: 'diff',
duplicateKeyPrefix: 'diff'
}
);
}, 250);
} catch (error) {
Expand Down
13 changes: 12 additions & 1 deletion src/web-ui/src/flow_chat/components/modern/SessionFilesBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@ export const SessionFilesBadge: React.FC<SessionFilesBadgeProps> = ({

try {
const diffData = await snapshotAPI.getOperationDiff(sessionId, filePath);
if ((diffData.originalContent || '') === (diffData.modifiedContent || '')) {
log.debug('Skipping empty session diff', { filePath, sessionId });
setIsExpanded(false);
return;
}
const fileName = filePath.split(/[/\\]/).pop() || filePath;

// Expand the right panel.
Expand All @@ -252,7 +257,13 @@ export const SessionFilesBadge: React.FC<SessionFilesBadgeProps> = ({
diffData.modifiedContent || '',
false,
'agent',
currentWorkspace?.rootPath
currentWorkspace?.rootPath,
undefined,
false,
{
titleKind: 'diff',
duplicateKeyPrefix: 'diff'
}
);
}, 250);

Expand Down
Loading
Loading