From a3e0f97de96acf7bc45f8b115d0a013cb9177a70 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Thu, 3 Sep 2026 08:28:32 -0400 Subject: [PATCH] feat(watcher): ungate file watcher + wire diff_version bump (#743) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three changes for external-change reactivity in the Files Changed tab: 1. Remove the OPENCODE_EXPERIMENTAL_FILEWATCHER enable-gate — the @parcel/watcher worktree watch now runs by default for git repos. The DISABLE gate is preserved as opt-out (OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER). 2. Wire the dead file.watcher.updated listener in session.tsx to bump diff_version for the active session with 1s trailing-edge debounce. This triggers a diff refetch when external changes are detected. 3. Clean up: remove the deprecated flag from flag.ts and the desktop app's env. Closes harmoniqs/amicode#743 --- packages/app/src/pages/session.tsx | 15 +++++++- packages/core/src/filesystem/watcher.ts | 2 +- packages/core/src/flag/flag.ts | 3 -- packages/core/test/filesystem/watcher.test.ts | 37 +++++++++++++++++-- packages/desktop/src/main/server.ts | 1 - 5 files changed, 49 insertions(+), 9 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index f40c172c5c..208a214c36 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1011,6 +1011,9 @@ export default function Page() { ), ) + // Bump diff_version when the watcher reports external changes to files in the + // current session's worktree (#743). Debounced: 1s trailing-edge per session. + let watcherDebounce: ReturnType | undefined const stopVcs = sdk().event.listen((evt) => { const details = evt.details as { type: string; properties?: unknown } if (details.type !== "file.watcher.updated" && details.type !== "filesystem.changed") return @@ -1020,8 +1023,18 @@ export default function Page() { : undefined const file = typeof props?.file === "string" ? props.file : undefined if (!file || file.startsWith(".git/")) return + const id = params.id + if (!id) return + if (watcherDebounce !== undefined) clearTimeout(watcherDebounce) + watcherDebounce = setTimeout(() => { + watcherDebounce = undefined + sync().set("diff_version", id, (v: number | undefined) => (v ?? 0) + 1) + }, 1000) + }) + onCleanup(() => { + stopVcs() + if (watcherDebounce !== undefined) clearTimeout(watcherDebounce) }) - onCleanup(stopVcs) createEffect( on( diff --git a/packages/core/src/filesystem/watcher.ts b/packages/core/src/filesystem/watcher.ts index c5e2063191..a9cff44b83 100644 --- a/packages/core/src/filesystem/watcher.ts +++ b/packages/core/src/filesystem/watcher.ts @@ -106,7 +106,7 @@ const layer = Layer.effect( const config = (yield* (yield* Config.Service).entries()) .filter((entry): entry is Config.Document => entry.type === "document") .flatMap((item) => item.info.watcher?.ignore ?? []) - if (location.vcs && (yield* Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER)) { + if (location.vcs) { yield* Effect.forkScoped( subscribe(location.directory, [...Ignore.PATTERNS, ...config, ...protecteds(location.directory)]), ) diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index 4c1d361694..4a7d58eb74 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -35,9 +35,6 @@ export const Flag = { OPENCODE_DISABLE_FFF: fff === undefined ? process.platform === "win32" : truthy("OPENCODE_DISABLE_FFF"), // Experimental - OPENCODE_EXPERIMENTAL_FILEWATCHER: Config.boolean("OPENCODE_EXPERIMENTAL_FILEWATCHER").pipe( - Config.withDefault(false), - ), OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: Config.boolean("OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER").pipe( Config.withDefault(false), ), diff --git a/packages/core/test/filesystem/watcher.test.ts b/packages/core/test/filesystem/watcher.test.ts index 0a287ea010..21909c7647 100644 --- a/packages/core/test/filesystem/watcher.test.ts +++ b/packages/core/test/filesystem/watcher.test.ts @@ -30,11 +30,13 @@ const configLayer = Layer.succeed( const flagsLayer = ConfigProvider.layer( ConfigProvider.fromUnknown({ - OPENCODE_EXPERIMENTAL_FILEWATCHER: "true", OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "false", }), ) +// Same flags — verifies the watcher runs without any explicit enable flag (#743). +const defaultFlagsLayer = flagsLayer + function provide(directory: string, vcs?: Location.Interface["vcs"]) { const locationLayer = Layer.succeed( Location.Service, @@ -48,10 +50,24 @@ function provide(directory: string, vcs?: Location.Interface["vcs"]) { ) } +function provideDefault(directory: string, vcs?: Location.Interface["vcs"]) { + const locationLayer = Layer.succeed( + Location.Service, + Location.Service.of(location({ directory: AbsolutePath.make(directory) }, { vcs })), + ) + return Effect.provide( + AppNodeBuilder.build(Watcher.node, [ + [Config.node, configLayer], + [Location.node, locationLayer], + ]).pipe(Layer.provide(defaultFlagsLayer)), + ) +} + function withTmp( f: (directory: string, vcs?: Location.Interface["vcs"]) => Effect.Effect, - options?: { git?: boolean; init?: (directory: string) => Promise }, + options?: { git?: boolean; init?: (directory: string) => Promise; useDefaultFlags?: boolean }, ) { + const providerFn = options?.useDefaultFlags ? provideDefault : provide return Effect.acquireRelease( Effect.promise(async () => { const tmp = await tmpdir() @@ -66,7 +82,7 @@ function withTmp( return { tmp, vcs: { type: "git" as const, store: AbsolutePath.make(path.join(tmp.path, ".git")) } } }), ({ tmp }) => Effect.promise(() => tmp[Symbol.asyncDispose]()), - ).pipe(Effect.flatMap(({ tmp, vcs }) => f(tmp.path, vcs).pipe(provide(tmp.path, vcs)))) + ).pipe(Effect.flatMap(({ tmp, vcs }) => f(tmp.path, vcs).pipe(providerFn(tmp.path, vcs)))) } function wait(check: (event: WatcherEvent) => boolean) { @@ -263,4 +279,19 @@ describeWatcher("Watcher", () => { ), ) }) + + it.live("runs by default for git repos without the enable flag (#743)", () => + withTmp( + (directory) => + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const file = path.join(directory, "default-watch.txt") + yield* ready(directory) + expect( + yield* nextUpdate((event) => event.file === file && event.event === "add", fs.writeFileString(file, "a")), + ).toEqual({ file, event: "add" }) + }), + { git: true, useDefaultFlags: true }, + ), + ) }) diff --git a/packages/desktop/src/main/server.ts b/packages/desktop/src/main/server.ts index ae1a98efdf..a9601205e7 100644 --- a/packages/desktop/src/main/server.ts +++ b/packages/desktop/src/main/server.ts @@ -47,7 +47,6 @@ export function preferAppEnv(userDataPath: string) { Object.assign(process.env, { ...shellEnv, OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true", - OPENCODE_EXPERIMENTAL_FILEWATCHER: "true", OPENCODE_CLIENT: "desktop", XDG_STATE_HOME: process.env.XDG_STATE_HOME ?? userDataPath, })