Skip to content

Use extension handle to identify extensions in file-watcher#7224

Open
isaacroldan wants to merge 1 commit intomainfrom
04-08-use_extension_handle_to_identify_extensions_in_file-watcher
Open

Use extension handle to identify extensions in file-watcher#7224
isaacroldan wants to merge 1 commit intomainfrom
04-08-use_extension_handle_to_identify_extensions_in_file-watcher

Conversation

@isaacroldan
Copy link
Copy Markdown
Contributor

@isaacroldan isaacroldan commented Apr 8, 2026

WHY are these changes introduced?

The file watcher was using extension directory paths to identify which extensions are affected by file changes. This approach has limitations when dealing with shared files or when extensions need to be uniquely identified beyond their filesystem location.

WHAT is this pull request doing?

Refactors the file watcher system to use extension handles instead of directory paths for tracking file ownership and identifying affected extensions. The changes include:

  • Added extensionHandle field to WatcherEvent interface to uniquely identify extensions
  • Modified file tracking to map file paths to extension handles rather than directory paths
  • Updated event filtering logic to use extension handles when available, falling back to directory paths for folder-level events
  • Enhanced test cases to verify the new event structure and handle multiple events correctly
  • Improved event deduplication to consider extension handles alongside path and type

The extensionPath field is retained for backward compatibility and folder-level events where no extension handle exists yet.

How to test your changes?

  1. Run the existing file watcher tests to ensure all functionality works as expected
  2. Test file changes in extensions to verify events are properly attributed to the correct extension handles
  3. Test extension creation/deletion scenarios to ensure folder-level events still work correctly
  4. Verify that shared files between extensions generate separate events for each affected extension

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've considered possible documentation changes
  • I've considered analytics changes to measure impact
  • The change is user-facing, so I've added a changelog entry with pnpm changeset add

Copy link
Copy Markdown
Contributor Author

isaacroldan commented Apr 8, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@isaacroldan isaacroldan marked this pull request as ready for review April 8, 2026 16:45
@isaacroldan isaacroldan requested a review from a team as a code owner April 8, 2026 16:45
Copilot AI review requested due to automatic review settings April 8, 2026 16:45
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the dev file-watcher event model to attribute file changes to extensions by extension handle rather than only by extension directory path, enabling correct handling of shared files across multiple extensions.

Changes:

  • Adds extensionHandle?: string to WatcherEvent and threads it through event creation/deduping.
  • Changes watched-file ownership tracking to map file paths → extension handles.
  • Updates watcher event handling logic (and tests) to support multiple events for shared files.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
packages/app/src/cli/services/dev/app-events/file-watcher.ts Emits watcher events with extensionHandle and tracks watched files by handle.
packages/app/src/cli/services/dev/app-events/file-watcher.test.ts Adjusts tests to allow multiple emitted events for a single FS change.
packages/app/src/cli/services/dev/app-events/app-event-watcher-handler.ts Resolves affected extensions by extensionHandle when present, otherwise by directory.
Comments suppressed due to low confidence (1)

packages/app/src/cli/services/dev/app-events/file-watcher.test.ts:383

  • When expectedEventCount is 2, the test still only asserts against actualEvents[0]. Since the watcher can emit multiple events in either order (and the whole point of this PR is to differentiate them via extensionHandle), this can become order-dependent/flaky and it also doesn't validate that extensionHandle is set correctly. Prefer asserting that actualEvents contains an event matching expectedEvent (and, when multiple events are expected, assert the full set of extensionHandle values or the full list of expected events) rather than assuming index 0.
            const calls = onChange.mock.calls
            const actualEvents = calls.find((call) => call[0].length > 0)?.[0]

            if (!actualEvents) {
              throw new Error('Expected onChange to be called with events, but all calls had empty arrays')
            }

            const eventCount = expectedEventCount ?? 1
            expect(actualEvents).toHaveLength(eventCount)
            const actualEvent = actualEvents[0]

            expect(actualEvent.type).toBe(expectedEvent.type)
            expect(actualEvent.path).toBe(normalizePath(expectedEvent.path))
            expect(actualEvent.extensionPath).toBe(normalizePath(expectedEvent.extensionPath))
            expect(Array.isArray(actualEvent.startTime)).toBe(true)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +161 to 176
const extensionResults = this.app.realExtensions.map((extension) => ({
extension,
watchedFiles: extension.watchedFiles(),
}))

const allFiles = new Set<string>()
for (const {extension, watchedFiles} of extensionResults) {
const extensionDir = normalizePath(extension.directory)
for (const file of watchedFiles) {
const normalizedPath = normalizePath(file)
allFiles.add(normalizedPath)

// Track which extensions watch this file
const extensionsSet = this.extensionWatchedFiles.get(normalizedPath) ?? new Set()
extensionsSet.add(extensionDir)
this.extensionWatchedFiles.set(normalizedPath, extensionsSet)
// Track which extension handles watch this file
const handlesSet = this.extensionWatchedFiles.get(normalizedPath) ?? new Set()
handlesSet.add(extension.handle)
this.extensionWatchedFiles.set(normalizedPath, handlesSet)
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getAllWatchedFiles() now iterates over app.realExtensions, which includes app config (configuration-experience) extensions whose directory is typically the app root (./). Since ExtensionInstance.watchedFiles() defaults to **/* when no custom watch paths are defined, this can cause the watcher to add (and attribute events to) a very large set of app-root files for config extensions, which looks like a performance regression and can produce incorrect extension update events. Consider reverting to app.nonConfigExtensions (or explicitly filtering out ext.isAppConfigExtension) when building the watched-file map so only real extension directories contribute watched files.

Copilot uses AI. Check for mistakes.
Comment on lines +234 to 245
const extension = event.extensionHandle
? this.app.realExtensions.find((ext) => ext.handle === event.extensionHandle)
: undefined
const watchPaths = extension?.watchedFiles()
const ignoreInstance = this.ignored[event.extensionPath]
const ignoreInstance = extension ? this.ignored[extension.directory] : undefined

if (watchPaths) {
const isAValidWatchedPath = watchPaths.some((pattern) => matchGlob(event.path, pattern))
return !isAValidWatchedPath
} else if (ignoreInstance) {
const relative = relativePath(event.extensionPath, event.path)
const relative = relativePath(extension!.directory, event.path)
return ignoreInstance.ignores(relative)
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateApp() stores ignore instances keyed by normalizePath(ext.directory), but shouldIgnoreEvent() now looks them up via this.ignored[extension.directory] (non-normalized). On Windows (or any mixed-separator input), this key mismatch can prevent .gitignore-based ignoring from being applied. Use a consistently-normalized key (e.g., normalizePath(extension.directory) or event.extensionPath) for the ignored map lookup, and likewise use the same normalized base when computing the relativePath.

Copilot uses AI. Check for mistakes.
@isaacroldan isaacroldan force-pushed the 04-08-use_extension_handle_to_identify_extensions_in_file-watcher branch 2 times, most recently from b5640a9 to 8053877 Compare April 8, 2026 17:12
Co-authored-by: Claude Code <claude-code@anthropic.com>
@isaacroldan isaacroldan force-pushed the 04-08-use_extension_handle_to_identify_extensions_in_file-watcher branch from 8053877 to cc5abaa Compare April 9, 2026 10:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants