From 692b700c0da4e68ca0b87b59484499f3f79b4d2b Mon Sep 17 00:00:00 2001 From: xintaofei Date: Mon, 20 Apr 2026 23:20:08 +0800 Subject: [PATCH] fix(file-tree): invalidate both cached target and ancestor, skip collapsed refetches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a deep directory was renamed or deleted externally and both the directory itself and a parent directory were cached, only the self entry was cleared — leaving the parent's children list holding a ghost reference to the old child. Walk up to the nearest cached ancestor in addition to the direct hit so both stale entries are dropped together. Also gate the follow-up getFileTree refetch on the directory still being expanded. Collapsed branches only need their cache cleared; they will re-hydrate naturally on the next expansion, which avoids unnecessary IPC traffic during FS event bursts. --- .../layout/aux-panel-file-tree-tab.tsx | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/components/layout/aux-panel-file-tree-tab.tsx b/src/components/layout/aux-panel-file-tree-tab.tsx index beb5b1f..be481cf 100644 --- a/src/components/layout/aux-panel-file-tree-tab.tsx +++ b/src/components/layout/aux-panel-file-tree-tab.tsx @@ -1107,19 +1107,26 @@ export function FileTreeTab() { for (const changed of paths) { const normalized = normalizeComparePath(changed) if (!normalized) continue - // Walk from the changed path up to the root, invalidating the - // nearest cached ancestor (or the path itself if a directory of - // that name is in the cache). Checking the path itself covers - // FS events that report the directory path directly. + // When the changed path is itself a cached directory (FS events + // that report the directory directly, e.g. a rename or a dir-level + // notification), its own entry is stale — invalidate it. + if (cache.has(normalized)) { + invalidated.add(normalized) + } + // Independently of the above, walk up to the nearest cached + // ancestor: the ancestor's children listing may also be stale + // (a child was added, removed, or renamed). Without this, cases + // where both a parent and child are cached leave the parent + // holding a ghost reference to the old child. let cursor = normalized while (cursor.length > 0) { - if (cache.has(cursor)) { - invalidated.add(cursor) - break - } const slash = cursor.lastIndexOf("/") const parent = slash === -1 ? "" : cursor.slice(0, slash) if (parent.length === 0) break + if (cache.has(parent)) { + invalidated.add(parent) + break + } cursor = parent } } @@ -1129,7 +1136,13 @@ export function FileTreeTab() { cache.delete(path) } if (!loader) return + // Skip refetching directories that are no longer expanded — their + // cleared cache will be re-hydrated on the next expansion via the + // expandedPaths effect. This avoids spurious getFileTree traffic + // for collapsed branches under bursty FS activity. + const expanded = expandedPathsRef.current for (const path of invalidated) { + if (!expanded.has(path)) continue void loader(path) } }