fix(file-tree): keep aux-panel tree in sync with filesystem changes

Deep directories (beyond the workspace snapshot's depth limit) relied on
a lazy-loaded override cache that was never invalidated, so agent-created
files, in-app deletes / renames / rollbacks, and external changes inside
expanded deep folders stayed invisible until the folder was reopened.

Propagate watcher `changed_paths` through the delta envelope and fire a
Meta event whenever FS activity doesn't alter the tree/git snapshots, so
the frontend can surgically invalidate affected cache entries and
re-fetch. Manual refresh (Reload from disk) clears the cache and
re-hydrates still-expanded deep dirs through the same path. Replayed
deltas after reconnect are forwarded to the same listeners.

Also split the combined workspace-state effect into tree / git / status
slices so unrelated state transitions (e.g. the 'resyncing' flip during
a refresh) no longer rebuild the entire node tree and cause a flash.
This commit is contained in:
xintaofei
2026-04-20 22:57:24 +08:00
parent c825291b1e
commit baf3b6e89f
4 changed files with 220 additions and 15 deletions

View File

@@ -48,6 +48,8 @@ pub struct WorkspaceDeltaEnvelope {
pub kind: String,
pub payload: Vec<WorkspaceDelta>,
pub requires_resync: bool,
#[serde(default)]
pub changed_paths: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
@@ -58,6 +60,8 @@ pub struct WorkspaceStateEvent {
pub kind: String,
pub payload: Vec<WorkspaceDelta>,
pub requires_resync: bool,
#[serde(default)]
pub changed_paths: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
@@ -108,6 +112,7 @@ impl WorkspaceStateCore {
kind: String,
payload: Vec<WorkspaceDelta>,
requires_resync: bool,
changed_paths: Vec<String>,
) -> WorkspaceStateEvent {
self.seq += 1;
@@ -120,6 +125,7 @@ impl WorkspaceStateCore {
kind: kind.clone(),
payload: payload.clone(),
requires_resync,
changed_paths: changed_paths.clone(),
};
self.push_recent_event(envelope);
@@ -130,6 +136,7 @@ impl WorkspaceStateCore {
kind,
payload,
requires_resync,
changed_paths,
}
}
@@ -713,6 +720,17 @@ async fn flush_watch_batch(
});
}
// Surface FS activity that doesn't otherwise change tree/git snapshots
// (e.g. files added/removed in a directory beyond WORKSPACE_TREE_MAX_DEPTH,
// or gitignored / non-git-repo changes). The envelope's `changed_paths`
// lets the frontend invalidate its lazy-loaded overrides for deep
// directories without waiting for a manual reload.
if payload.is_empty() && !changed_paths.is_empty() {
payload.push(WorkspaceDelta::Meta {
reason: "fs_events".to_string(),
});
}
if payload.is_empty() {
return;
}
@@ -731,7 +749,7 @@ async fn flush_watch_batch(
"meta".to_string()
};
guard.append_event(kind, payload, git_presence_changed)
guard.append_event(kind, payload, git_presence_changed, changed_paths)
};
emit_event(emitter, "folder://workspace-state-event", event);
@@ -1072,6 +1090,7 @@ mod tests {
reason: "boot".to_string(),
}],
false,
Vec::new(),
);
let e2 = core.append_event(
@@ -1080,6 +1099,7 @@ mod tests {
reason: "tick".to_string(),
}],
false,
Vec::new(),
);
assert!(e2.seq > e1.seq);
@@ -1095,6 +1115,7 @@ mod tests {
reason: "a".to_string(),
}],
false,
Vec::new(),
);
core.append_event(
@@ -1103,6 +1124,7 @@ mod tests {
reason: "b".to_string(),
}],
false,
Vec::new(),
);
let snapshot = core.snapshot(Some(e1.seq));
@@ -1123,6 +1145,7 @@ mod tests {
reason: "a".to_string(),
}],
false,
Vec::new(),
);
core.append_event(
"meta".to_string(),
@@ -1130,6 +1153,7 @@ mod tests {
reason: "b".to_string(),
}],
false,
Vec::new(),
);
let snapshot = core.snapshot(Some(0));