Skip to content

Turn requestfilesystem into a single layer providing project.FileHandle lookups so it can be used as the top layer above editor overlays - #64263

Closed
Andrew Branch (andrewbranch) wants to merge 12 commits into
microsoft:mainfrom
andrewbranch:agents/vfs-layered-filesystem-refactor
Closed

Andrew Branch (andrewbranch) wants to merge 12 commits into
microsoft:mainfrom
andrewbranch:agents/vfs-layered-filesystem-refactor

Conversation

@andrewbranch

Copy link
Copy Markdown
Member

After #64115 was merged, I realized its approach had a surprising interaction with editor overlays when using the LSP-connected API. If you get an LSP-based snapshot and then do

const newSnapshot = lspSnapshot.update({
  fileSystem: createFileSystemLayer([
    [{ uri: "file:///home/andrew/project/src/main.ts" }, "// Updated content"]
  ])
});

you would clearly expect the newSnapshot to have the layer-provided contents for main.ts, but that will only be true if main.ts is not open in the editor.

#64115 works by creating a full vfs.FS view of the layer contents falling back to the host filesystem (if the layer is configured to fall back) which it passes to the project system, where SnapshotFS does its own layering: editor overlays on top of its cached files on top of the vfs.FS created by #64115, passed in by the API request. That's not the desired layer ordering, and a vfs.FS doesn’t provide enough info to split the actual API-provided layer apart from the host filesystem so the layers can be re-stacked as desired.

So, this PR refactors requestfilesystem to return a single layer providing the new project.FileSourceLayer interface that gives SnapshotFS enough info upon lookup to be able to stack that layer on top of the other layers it owns.

Copilot AI balanced review requested due to automatic review settings September 14, 2026 17:41
@github-project-automation github-project-automation Bot moved this to Not started in PR Backlog Sep 14, 2026
@typescript-automation typescript-automation Bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Sep 14, 2026

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Shared API sessions can switch layers without invalidating reused programs, producing inconsistent snapshot and program contents.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 21/21 changed files
  • Comments generated: 1
  • Review effort level: Balanced (auto)

Note

Copilot is running an experiment and ran this review at Balanced.

// The legacy updateSnapshot API restarts from the host when no base snapshot
// handle is supplied, rather than inheriting the canonical snapshot's layer.
// This workaround is removed by the snapshot state redesign in #64154.
fileSystemLayer, layerChanges, err := requestfilesystem.NewForUpdate(params.FileSystem, baseFileSystemLayer, baseSD == nil, s.fileSystem(), s.currentDirectory())

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Don't care, this is going away in the next PR in the stack

export interface CreateFileSystemOptions {
/** Complete directory listings. Full filesystems derive these from `files` when omitted. */
/**
* Complete `getAccessibleEntries` results. These do not constrain direct descendant lookups.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not clear on what this means.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Like

Subsequent lookups are not limited to the entries provided. Files may be "opened", or a file may exist on the host file system.

?

)

func TestFileChangesIncludeDirectoryTombstones(t *testing.T) {
func changeMap(changes []project.FileSourceLayerChange) map[string][2]bool {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Are you just doing this to statically avoid duplicates? You could just do a list of FileSourceLayerChange and make it clear which fields are explicitly true.

Comment on lines +94 to 97
result := make([]project.FileSourceLayerChange, 0, len(changes))
for _, change := range changes {
result = append(result, change)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
result := make([]project.FileSourceLayerChange, 0, len(changes))
for _, change := range changes {
result = append(result, change)
}
result := slices.Clone(changes)
}

for _, alias := range baseRequestFS.aliasesForPath(fileName) {
addChange(alias, deleted)
}
shadowsRequestSymlinks := func(fileName string) bool {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What does this name of this mean? Is requestShadowsSymlinks a better name? pathShadowsHostSymlink?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Okay, sorry, I didn't notice this was in requestfilesystem so I guess this is like shadowsReqFsSymlink?

Comment on lines +73 to +77
if shadowsRequestSymlinks(absolutePath) {
return project.FileSourceLayerChanges{InvalidateAll: true}
}
addRequestDescendants(absolutePath)
addWithAliases(absolutePath, true, true)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There's really nothing else to be done for a "removed" file? Maybe I'm out of my depth, but how is removing a host file done with these recursive calls?

@github-project-automation github-project-automation Bot moved this from Not started to Done in PR Backlog Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants