Skip to content

Commit 1eb8f97

Browse files
authored
Merge pull request #283 from wgqqqqq/fix/session-diff-created-files
Fix session diff handling for created files
2 parents e6eca37 + ba847b7 commit 1eb8f97

File tree

9 files changed

+204
-54
lines changed

9 files changed

+204
-54
lines changed

src/crates/core/src/service/snapshot/manager.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,8 @@ impl WrappedTool {
509509
debug!("Creating new file: file_path={}", file_path.display());
510510
}
511511

512+
let file_existed_before = file_path.exists();
513+
let operation_type = self.get_operation_type_internal(file_existed_before);
512514
let turn_index = self.extract_turn_index(context);
513515

514516
let snapshot_service = snapshot_manager.get_snapshot_service();
@@ -520,7 +522,7 @@ impl WrappedTool {
520522
self.name(),
521523
input.clone(),
522524
&file_path,
523-
self.get_operation_type_internal(),
525+
operation_type,
524526
context.tool_call_id.clone(),
525527
)
526528
.await
@@ -577,8 +579,15 @@ impl WrappedTool {
577579
}
578580

579581
/// Returns the operation type.
580-
fn get_operation_type_internal(&self) -> OperationType {
582+
fn get_operation_type_internal(&self, file_existed_before: bool) -> OperationType {
581583
match self.name() {
584+
"Write" | "write_file" => {
585+
if file_existed_before {
586+
OperationType::Modify
587+
} else {
588+
OperationType::Create
589+
}
590+
}
582591
"create_file" => OperationType::Create,
583592
"delete_file" | "Delete" => OperationType::Delete,
584593
"rename_file" | "move_file" => OperationType::Rename,

src/crates/core/src/service/snapshot/snapshot_core.rs

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,9 @@ impl SnapshotCore {
130130
None
131131
};
132132

133-
if let Some(before_id) = &before_snapshot_id {
134-
if !self.snapshot_system.has_baseline(&file_path).await {
135-
match self
133+
if !self.snapshot_system.has_baseline(&file_path).await {
134+
match &before_snapshot_id {
135+
Some(before_id) => match self
136136
.snapshot_system
137137
.create_baseline_from_snapshot(&file_path, before_id)
138138
.await
@@ -149,13 +149,30 @@ impl SnapshotCore {
149149
file_path, e
150150
);
151151
}
152+
},
153+
None if operation_type == OperationType::Create => {
154+
match self.snapshot_system.create_empty_baseline(&file_path).await {
155+
Ok(baseline_id) => {
156+
debug!(
157+
"Created empty baseline snapshot for new file: file_path={:?} baseline_id={}",
158+
file_path, baseline_id
159+
);
160+
}
161+
Err(e) => {
162+
warn!(
163+
"Failed to create empty baseline snapshot: file_path={:?} error={}",
164+
file_path, e
165+
);
166+
}
167+
}
152168
}
153-
} else {
154-
debug!(
155-
"Baseline snapshot already exists: file_path={:?}",
156-
file_path
157-
);
169+
None => {}
158170
}
171+
} else {
172+
debug!(
173+
"Baseline snapshot already exists: file_path={:?}",
174+
file_path
175+
);
159176
}
160177

161178
let session = self
@@ -431,6 +448,11 @@ impl SnapshotCore {
431448
return Err(SnapshotError::SessionNotFound(session_id.to_string()));
432449
};
433450

451+
let first_op_for_file = session
452+
.all_operations_iter()
453+
.find(|op| op.file_path == file_path);
454+
let file_created_in_session = matches!(first_op_for_file, Some(op) if op.before_snapshot_id.is_none());
455+
434456
let load_first_before = || async {
435457
let first_before = session
436458
.all_operations_iter()
@@ -447,31 +469,39 @@ impl SnapshotCore {
447469
.unwrap_or_default()
448470
};
449471

450-
let before = if let Some(baseline_id) = self
451-
.snapshot_system
452-
.get_baseline_snapshot_id(file_path)
453-
.await
454-
{
472+
let before = if file_created_in_session {
455473
debug!(
456-
"Using baseline snapshot for diff: file_path={:?} baseline_id={}",
457-
file_path, baseline_id
474+
"Using empty baseline for session-created file diff: file_path={:?} session_id={}",
475+
file_path, session_id
458476
);
459-
match self
477+
String::new()
478+
} else {
479+
if let Some(baseline_id) = self
460480
.snapshot_system
461-
.get_snapshot_content(&baseline_id)
481+
.get_baseline_snapshot_id(file_path)
462482
.await
463483
{
464-
Ok(content) => content,
465-
Err(e) => {
466-
warn!(
467-
"Failed to read baseline snapshot, falling back to first before snapshot: baseline_id={} error={}",
468-
baseline_id, e
469-
);
470-
load_first_before().await
484+
debug!(
485+
"Using baseline snapshot for diff: file_path={:?} baseline_id={}",
486+
file_path, baseline_id
487+
);
488+
match self
489+
.snapshot_system
490+
.get_snapshot_content(&baseline_id)
491+
.await
492+
{
493+
Ok(content) => content,
494+
Err(e) => {
495+
warn!(
496+
"Failed to read baseline snapshot, falling back to first before snapshot: baseline_id={} error={}",
497+
baseline_id, e
498+
);
499+
load_first_before().await
500+
}
471501
}
502+
} else {
503+
load_first_before().await
472504
}
473-
} else {
474-
load_first_before().await
475505
};
476506

477507
let after = if file_path.exists() {

src/crates/core/src/service/snapshot/snapshot_system.rs

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,46 @@ impl BaselineCache {
164164

165165
Ok(baseline_id)
166166
}
167+
168+
/// Creates an empty baseline for files that are first introduced during the session.
169+
pub async fn create_empty(
170+
&self,
171+
file_path: &Path,
172+
empty_content_hash: &str,
173+
content_path: &Path,
174+
) -> SnapshotResult<String> {
175+
let baseline_id = format!("baseline_empty_{}", Uuid::new_v4());
176+
177+
if !content_path.exists() {
178+
fs::write(content_path, [])?;
179+
}
180+
181+
let baseline_metadata = FileSnapshot {
182+
snapshot_id: baseline_id.clone(),
183+
file_path: file_path.to_path_buf(),
184+
content_hash: empty_content_hash.to_string(),
185+
snapshot_type: SnapshotType::Baseline,
186+
compressed_content: Vec::new(),
187+
timestamp: SystemTime::now(),
188+
metadata: FileMetadata {
189+
size: 0,
190+
permissions: None,
191+
last_modified: SystemTime::now(),
192+
encoding: "utf-8".to_string(),
193+
},
194+
};
195+
196+
let baseline_meta_path = self.baseline_dir.join(format!("{}.json", baseline_id));
197+
let metadata_json = serde_json::to_string_pretty(&baseline_metadata)?;
198+
fs::write(&baseline_meta_path, metadata_json)?;
199+
200+
{
201+
let mut cache = self.cache.write().await;
202+
cache.insert(file_path.to_path_buf(), Some(baseline_id.clone()));
203+
}
204+
205+
Ok(baseline_id)
206+
}
167207
}
168208

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

304344
if self.dedup_enabled && self.hash_to_path.contains_key(&content_hash) {
345+
if let Some(snapshot_id) = self.find_snapshot_by_hash(&content_hash) {
346+
debug!(
347+
"Found duplicate content, reusing existing snapshot: content_hash={}",
348+
content_hash
349+
);
350+
return Ok(snapshot_id);
351+
}
352+
305353
debug!(
306-
"Found duplicate content, reusing existing snapshot: content_hash={}",
354+
"Found reusable content without active snapshot metadata, creating new snapshot metadata: content_hash={}",
307355
content_hash
308356
);
309-
return Ok(self.find_snapshot_by_hash(&content_hash)?);
310357
}
311358

312359
let optimized_content = self.optimize_content(&content);
@@ -473,17 +520,13 @@ impl FileSnapshotSystem {
473520
}
474521

475522
/// Finds a snapshot ID by hash.
476-
fn find_snapshot_by_hash(&self, content_hash: &str) -> SnapshotResult<String> {
523+
fn find_snapshot_by_hash(&self, content_hash: &str) -> Option<String> {
477524
for (snapshot_id, snapshot) in &self.active_snapshots {
478525
if snapshot.content_hash == content_hash {
479-
return Ok(snapshot_id.clone());
526+
return Some(snapshot_id.clone());
480527
}
481528
}
482-
483-
Err(SnapshotError::SnapshotNotFound(format!(
484-
"hash: {}",
485-
content_hash
486-
)))
529+
None
487530
}
488531

489532
/// Loads snapshot metadata from disk (without using in-memory cache).
@@ -778,6 +821,21 @@ impl FileSnapshotSystem {
778821
.await
779822
}
780823

824+
/// Creates an empty baseline for files that did not exist before the session.
825+
pub async fn create_empty_baseline(&mut self, file_path: &Path) -> SnapshotResult<String> {
826+
let empty_content_hash = self.calculate_content_hash(&[]);
827+
let content_path = self.get_content_path(&empty_content_hash);
828+
829+
if !self.hash_to_path.contains_key(&empty_content_hash) {
830+
self.hash_to_path
831+
.insert(empty_content_hash.clone(), content_path.clone());
832+
}
833+
834+
self.baseline_cache
835+
.create_empty(file_path, &empty_content_hash, &content_path)
836+
.await
837+
}
838+
781839
/// Checks whether the file has a baseline.
782840
pub async fn has_baseline(&self, file_path: &Path) -> bool {
783841
self.get_baseline_snapshot_id(file_path).await.is_some()

src/web-ui/src/flow_chat/components/modern/SessionFileModificationsBar.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,10 @@ export const SessionFileModificationsBar: React.FC<SessionFileModificationsBarPr
239239

240240
try {
241241
const diffData = await snapshotAPI.getOperationDiff(sessionId, filePath);
242+
if ((diffData.originalContent || '') === (diffData.modifiedContent || '')) {
243+
log.debug('Skipping empty session diff', { filePath, sessionId });
244+
return;
245+
}
242246
const fileName = filePath.split(/[/\\]/).pop() || filePath;
243247

244248
window.dispatchEvent(new CustomEvent('expand-right-panel'));
@@ -251,7 +255,13 @@ export const SessionFileModificationsBar: React.FC<SessionFileModificationsBarPr
251255
diffData.modifiedContent || '',
252256
false,
253257
'agent',
254-
currentWorkspace?.rootPath
258+
currentWorkspace?.rootPath,
259+
undefined,
260+
false,
261+
{
262+
titleKind: 'diff',
263+
duplicateKeyPrefix: 'diff'
264+
}
255265
);
256266
}, 250);
257267
} catch (error) {

src/web-ui/src/flow_chat/components/modern/SessionFilesBadge.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,11 @@ export const SessionFilesBadge: React.FC<SessionFilesBadgeProps> = ({
239239

240240
try {
241241
const diffData = await snapshotAPI.getOperationDiff(sessionId, filePath);
242+
if ((diffData.originalContent || '') === (diffData.modifiedContent || '')) {
243+
log.debug('Skipping empty session diff', { filePath, sessionId });
244+
setIsExpanded(false);
245+
return;
246+
}
242247
const fileName = filePath.split(/[/\\]/).pop() || filePath;
243248

244249
// Expand the right panel.
@@ -252,7 +257,13 @@ export const SessionFilesBadge: React.FC<SessionFilesBadgeProps> = ({
252257
diffData.modifiedContent || '',
253258
false,
254259
'agent',
255-
currentWorkspace?.rootPath
260+
currentWorkspace?.rootPath,
261+
undefined,
262+
false,
263+
{
264+
titleKind: 'diff',
265+
duplicateKeyPrefix: 'diff'
266+
}
256267
);
257268
}, 250);
258269

0 commit comments

Comments
 (0)