From 86a2595ac564383be967534908e3d67db8e709e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=91=E9=98=B3?= <56574505+lxqddd@users.noreply.github.com> Date: Tue, 15 Sep 2026 15:27:28 +0800 Subject: [PATCH] fix(app): refresh review panel on external git commits The review panel caches its vcs.diff query with every refetch hook disabled, so it only refreshes through explicit invalidation. Most terminal git operations still refresh it by accident because they rewrite workspace files, but a git commit/add/amend only rewrites files under .git, which produced no event: - the server's .git subscription watches only HEAD, and a regular commit does not change HEAD's content - the client's session VCS listener dropped every .git path anyway (that check also compared a relative prefix against absolute event paths, so it never actually matched) The panel then kept showing pre-commit changes until an unrelated event happened to refresh it. - watch .git/index alongside HEAD in the core watcher (staging and commits rewrite it; everything else under .git stays ignored) - in the session VCS listener, treat paths under .git whose basename is index or HEAD as VCS state changes and call the existing debounced refreshVcs(); basename matching also covers git worktrees, where the path is .git/worktrees//index - flip the watcher test to assert .git/index events are published and add coverage that other .git files remain ignored Co-Authored-By: Claude Code --- packages/app/src/pages/session.tsx | 10 ++++++- packages/core/src/filesystem/watcher.ts | 5 +++- packages/core/test/filesystem/watcher.test.ts | 29 ++++++++++++++----- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 2e647e0f4789..dc3478dde782 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -957,7 +957,15 @@ export default function Page() { ? (details.properties as Record) : undefined const file = typeof props?.file === "string" ? props.file : undefined - if (!file || file.startsWith(".git/")) return + if (!file) return + // .git is watched separately; rewrites of index (stage/commit) and HEAD + // (branch switch) change the VCS state the review panel renders, so refresh + // for those and skip the rest of .git + const segments = file.split(/[\\/]/) + if (segments.includes(".git")) { + const name = segments[segments.length - 1] + if (name !== "index" && name !== "HEAD") return + } refreshVcs() }) onCleanup(stopVcs) diff --git a/packages/core/src/filesystem/watcher.ts b/packages/core/src/filesystem/watcher.ts index c5e20631917e..78badeb14a22 100644 --- a/packages/core/src/filesystem/watcher.ts +++ b/packages/core/src/filesystem/watcher.ts @@ -116,8 +116,11 @@ const layer = Layer.effect( const resolved = (yield* git.repo.discover(location.directory))?.gitDirectory const vcs = resolved ? yield* fs.realPath(resolved).pipe(Effect.catch(() => Effect.succeed(resolved))) : undefined if (vcs && !config.includes(".git") && !config.includes(vcs) && (!resolved || !config.includes(resolved))) { + // watch HEAD (branch switches) and index (staging/commits) so clients can + // refresh VCS state after out-of-band git commands; everything else under + // .git is noise const ignore = (yield* fs.readDirectoryEntries(vcs).pipe(Effect.catch(() => Effect.succeed([])))).flatMap( - (entry) => (entry.name === "HEAD" ? [] : [entry.name]), + (entry) => (entry.name === "HEAD" || entry.name === "index" ? [] : [entry.name]), ) yield* Effect.forkScoped(subscribe(vcs, ignore)) } diff --git a/packages/core/test/filesystem/watcher.test.ts b/packages/core/test/filesystem/watcher.test.ts index 0a287ea010d9..99123540968b 100644 --- a/packages/core/test/filesystem/watcher.test.ts +++ b/packages/core/test/filesystem/watcher.test.ts @@ -197,19 +197,34 @@ describeWatcher("Watcher", () => { }).pipe(Effect.provide(AppNodeBuilder.build(LayerNode.group([FSUtil.node, EventV2.node])))), ) - it.live("ignores .git/index changes", () => + it.live("publishes .git/index events", () => withTmp( (directory) => Effect.gen(function* () { const fs = yield* FSUtil.Service const index = path.join(directory, ".git", "index") yield* ready(directory) - yield* noUpdate( - (event) => event.file === index, - fs - .writeFileString(path.join(directory, "tracked.txt"), "a") - .pipe(Effect.andThen(Effect.promise(() => $`git add .`.cwd(directory).quiet())), Effect.asVoid), - ) + expect( + yield* nextUpdate( + (event) => event.file === index, + fs + .writeFileString(path.join(directory, "tracked.txt"), "a") + .pipe(Effect.andThen(Effect.promise(() => $`git add .`.cwd(directory).quiet())), Effect.asVoid), + ), + ).toMatchObject({ file: index }) + }), + { git: true }, + ), + ) + + it.live("ignores other .git changes", () => + withTmp( + (directory) => + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const msg = path.join(directory, ".git", "COMMIT_EDITMSG") + yield* ready(directory) + yield* noUpdate((event) => event.file === msg, fs.writeFileString(msg, "test")) }), { git: true }, ),