From bec7b2ba7d338b232aceb656ff5b2f2eeed52b2d Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Mon, 2 Mar 2026 16:48:34 -0800 Subject: [PATCH] Sessions window: GH file system provider fixes --- .../changesView/browser/changesView.ts | 15 +++- .../contrib/chat/browser/repoPicker.ts | 3 +- .../fileTreeView/browser/fileTreeView.ts | 18 +++-- .../browser/githubFileSystemProvider.ts | 76 +++++++++++++++++-- .../browser/sessionsManagementService.ts | 4 +- .../browser/workspaceFolderManagement.ts | 14 ++-- 6 files changed, 110 insertions(+), 20 deletions(-) diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index 47eca5b0933..76f765c4096 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -57,6 +57,7 @@ import { IExtensionService } from '../../../../workbench/services/extensions/com import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/browser/layoutService.js'; import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js'; +import { GITHUB_REMOTE_FILE_SCHEME } from '../../fileTreeView/browser/githubFileSystemProvider.js'; const $ = dom.$; @@ -125,7 +126,19 @@ function buildTreeChildren(items: IChangesFileItem[]): IObjectTreeElement= 3) { + dirPath = '/' + parts.slice(3).join('/'); + } else { + dirPath = '/'; + } + } + const segments = dirPath.split('/').filter(Boolean); let current = root; diff --git a/src/vs/sessions/contrib/chat/browser/repoPicker.ts b/src/vs/sessions/contrib/chat/browser/repoPicker.ts index 95e8b039df5..bcd4506ea3a 100644 --- a/src/vs/sessions/contrib/chat/browser/repoPicker.ts +++ b/src/vs/sessions/contrib/chat/browser/repoPicker.ts @@ -15,6 +15,7 @@ import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js' import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { INewSession } from './newSession.js'; import { URI } from '../../../../base/common/uri.js'; +import { GITHUB_REMOTE_FILE_SCHEME } from '../../fileTreeView/browser/githubFileSystemProvider.js'; const OPEN_REPO_COMMAND = 'github.copilot.chat.cloudSessions.openRepository'; const STORAGE_KEY_LAST_REPO = 'agentSessions.lastPickedRepo'; @@ -270,7 +271,7 @@ export class RepoPicker extends Disposable { } private _setRepo(repo: IRepoItem): void { - this._newSession?.setRepoUri(URI.parse(`vscode-vfs://github/${repo.id}`)); + this._newSession?.setRepoUri(URI.parse(`${GITHUB_REMOTE_FILE_SCHEME}://github/${repo.id}/HEAD`)); } } diff --git a/src/vs/sessions/contrib/fileTreeView/browser/fileTreeView.ts b/src/vs/sessions/contrib/fileTreeView/browser/fileTreeView.ts index 0ef75870432..56d712dbef9 100644 --- a/src/vs/sessions/contrib/fileTreeView/browser/fileTreeView.ts +++ b/src/vs/sessions/contrib/fileTreeView/browser/fileTreeView.ts @@ -43,7 +43,7 @@ import { IStorageService } from '../../../../platform/storage/common/storage.js' import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { ISessionsManagementService, IActiveSessionItem } from '../../sessions/browser/sessionsManagementService.js'; -import { GITHUB_REMOTE_FILE_SCHEME } from './githubFileSystemProvider.js'; +import { GITHUB_REMOTE_FILE_SCHEME, getGitHubRemoteFileDisplayName } from './githubFileSystemProvider.js'; import { basename } from '../../../../base/common/path.js'; import { isEqual } from '../../../../base/common/resources.js'; @@ -315,6 +315,8 @@ export class FileTreeViewPane extends ViewPane { * Extracts a github-remote-file:// URI from session metadata, trying various known fields. */ private extractRepoUriFromMetadata(metadata: { readonly [key: string]: unknown }): URI | undefined { + const ref = metadata?.branch || 'HEAD'; + // repositoryNwo: "owner/repo" const repositoryNwo = metadata.repositoryNwo as string | undefined; if (repositoryNwo && repositoryNwo.includes('/')) { @@ -322,7 +324,7 @@ export class FileTreeViewPane extends ViewPane { return URI.from({ scheme: GITHUB_REMOTE_FILE_SCHEME, authority: 'github', - path: `/${repositoryNwo}/HEAD`, + path: `/${repositoryNwo}/${ref}`, }); } @@ -335,7 +337,7 @@ export class FileTreeViewPane extends ViewPane { return URI.from({ scheme: GITHUB_REMOTE_FILE_SCHEME, authority: 'github', - path: `/${parsed.owner}/${parsed.repo}/HEAD`, + path: `/${parsed.owner}/${parsed.repo}/${ref}`, }); } } @@ -349,7 +351,7 @@ export class FileTreeViewPane extends ViewPane { return URI.from({ scheme: GITHUB_REMOTE_FILE_SCHEME, authority: 'github', - path: `/${repository}/HEAD`, + path: `/${repository}/${ref}`, }); } const parsed = this.parseGitHubUrl(repository); @@ -358,7 +360,7 @@ export class FileTreeViewPane extends ViewPane { return URI.from({ scheme: GITHUB_REMOTE_FILE_SCHEME, authority: 'github', - path: `/${parsed.owner}/${parsed.repo}/HEAD`, + path: `/${parsed.owner}/${parsed.repo}/${ref}`, }); } } @@ -514,7 +516,7 @@ export class FileTreeViewPane extends ViewPane { if (this.tree && rootUri && !isEqual(rootUri, lastRootUri)) { lastRootUri = rootUri; - this.updateTitle(basename(rootUri.path) || rootUri.toString()); + this.updateTitle(this.getTreeTitle(rootUri)); this.treeInputDisposable.clear(); this.tree.setInput(rootUri).then(() => { this.layoutTree(); @@ -525,6 +527,10 @@ export class FileTreeViewPane extends ViewPane { })); } + private getTreeTitle(rootUri: URI): string { + return getGitHubRemoteFileDisplayName(rootUri) ?? (basename(rootUri.path) || rootUri.toString()); + } + private layoutTree(): void { if (!this.tree) { return; diff --git a/src/vs/sessions/contrib/fileTreeView/browser/githubFileSystemProvider.ts b/src/vs/sessions/contrib/fileTreeView/browser/githubFileSystemProvider.ts index 289911e3995..daf651578a9 100644 --- a/src/vs/sessions/contrib/fileTreeView/browser/githubFileSystemProvider.ts +++ b/src/vs/sessions/contrib/fileTreeView/browser/githubFileSystemProvider.ts @@ -14,6 +14,23 @@ import { ILogService } from '../../../../platform/log/common/log.js'; export const GITHUB_REMOTE_FILE_SCHEME = 'github-remote-file'; +/** + * Derives a display name from a github-remote-file URI. + * Returns "repo (branch)" or just "repo" when on HEAD. + */ +export function getGitHubRemoteFileDisplayName(uri: URI): string | undefined { + if (uri.scheme !== GITHUB_REMOTE_FILE_SCHEME) { + return undefined; + } + const parts = uri.path.split('/').filter(Boolean); + // path = /{owner}/{repo}/{ref} + if (parts.length >= 3) { + const [, ref,] = parts; + return ref; + } + return undefined; +} + /** * GitHub REST API response for the Trees endpoint. * GET /repos/{owner}/{repo}/git/trees/{tree_sha}?recursive=1 @@ -67,9 +84,18 @@ export class GitHubFileSystemProvider extends Disposable implements IFileSystemP /** Cache keyed by "owner/repo/ref" */ private readonly treeCache = new Map(); + /** Negative cache for refs that returned 404, keyed by "owner/repo/ref" */ + private readonly notFoundCache = new Map(); + + /** In-flight fetch promises keyed by "owner/repo/ref" to deduplicate concurrent requests */ + private readonly pendingFetches = new Map>(); + /** Cache TTL - 5 minutes */ private static readonly CACHE_TTL_MS = 5 * 60 * 1000; + /** Negative cache TTL - 1 minute */ + private static readonly NOT_FOUND_CACHE_TTL_MS = 60 * 1000; + constructor( @IRequestService private readonly requestService: IRequestService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, @@ -107,23 +133,46 @@ export class GitHubFileSystemProvider extends Disposable implements IFileSystemP // --- GitHub API private async getAuthToken(): Promise { - const sessions = await this.authenticationService.getSessions('github', ['repo']); + // Try to get an existing session silently before prompting the user + const sessions = await this.authenticationService.getSessions('github', ['repo'], {}, true); if (sessions.length > 0) { return sessions[0].accessToken; } - // Try to create a session if none exists + // No existing session found — create one (may prompt the user) const session = await this.authenticationService.createSession('github', ['repo']); return session.accessToken; } - private async fetchTree(owner: string, repo: string, ref: string): Promise { + private fetchTree(owner: string, repo: string, ref: string): Promise { const cacheKey = this.getCacheKey(owner, repo, ref); + + // Check positive cache const cached = this.treeCache.get(cacheKey); if (cached && (Date.now() - cached.fetchedAt) < GitHubFileSystemProvider.CACHE_TTL_MS) { - return cached; + return Promise.resolve(cached); } + // Check negative cache (recently returned 404) + const notFoundAt = this.notFoundCache.get(cacheKey); + if (notFoundAt !== undefined && (Date.now() - notFoundAt) < GitHubFileSystemProvider.NOT_FOUND_CACHE_TTL_MS) { + return Promise.reject(createFileSystemProviderError(`Tree not found for ${owner}/${repo}@${ref}`, FileSystemProviderErrorCode.FileNotFound)); + } + + // Deduplicate concurrent requests for the same tree + const pending = this.pendingFetches.get(cacheKey); + if (pending) { + return pending; + } + + const promise = this.doFetchTree(owner, repo, ref, cacheKey).finally(() => { + this.pendingFetches.delete(cacheKey); + }); + this.pendingFetches.set(cacheKey, promise); + return promise; + } + + private async doFetchTree(owner: string, repo: string, ref: string, cacheKey: string): Promise { this.logService.info(`[SessionRepoFS] Fetching tree for ${owner}/${repo}@${ref}`); const token = await this.getAuthToken(); @@ -138,7 +187,18 @@ export class GitHubFileSystemProvider extends Disposable implements IFileSystemP }, }, CancellationToken.None); - const data = await asJson(response); + let data: IGitHubTreeResponse | null; + try { + data = await asJson(response); + } catch (err) { + // Cache 404s so we don't keep re-fetching missing trees + if (err instanceof Error && err.message.includes('404')) { + this.notFoundCache.set(cacheKey, Date.now()); + throw createFileSystemProviderError(`Tree not found for ${owner}/${repo}@${ref}`, FileSystemProviderErrorCode.FileNotFound); + } + throw err; + } + if (!data) { throw createFileSystemProviderError(`Failed to fetch tree for ${owner}/${repo}@${ref}`, FileSystemProviderErrorCode.Unavailable); } @@ -283,11 +343,15 @@ export class GitHubFileSystemProvider extends Disposable implements IFileSystemP // --- Cache management invalidateCache(owner: string, repo: string, ref: string): void { - this.treeCache.delete(this.getCacheKey(owner, repo, ref)); + const cacheKey = this.getCacheKey(owner, repo, ref); + this.treeCache.delete(cacheKey); + this.notFoundCache.delete(cacheKey); } override dispose(): void { this.treeCache.clear(); + this.notFoundCache.clear(); + this.pendingFetches.clear(); super.dispose(); } } diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts index 6e830f21c67..58d91f9b95d 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts @@ -24,6 +24,7 @@ import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browse import { INewSession, LocalNewSession, RemoteNewSession } from '../../chat/browser/newSession.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { ILanguageModelsService } from '../../../../workbench/contrib/chat/common/languageModels.js'; +import { GITHUB_REMOTE_FILE_SCHEME } from '../../fileTreeView/browser/githubFileSystemProvider.js'; export const IsNewChatSessionContext = new RawContextKey('isNewChatSession', true); @@ -202,7 +203,8 @@ export class SessionsManagementService extends Disposable implements ISessionsMa } if (session.providerType === AgentSessionProviders.Cloud) { - return [URI.parse(`vscode-vfs://github/${metadata.owner}/${metadata.name}`), undefined]; + const ref = metadata?.branch || 'HEAD'; + return [URI.parse(`${GITHUB_REMOTE_FILE_SCHEME}://github/${metadata.owner}/${metadata.name}/${ref}`), undefined]; } const workingDirectoryPath = metadata?.workingDirectoryPath as string | undefined; diff --git a/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts b/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts index 4e310aecaf8..581a241e564 100644 --- a/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts +++ b/src/vs/sessions/contrib/workspace/browser/workspaceFolderManagement.ts @@ -14,6 +14,7 @@ import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uri import { URI } from '../../../../base/common/uri.js'; import { autorun } from '../../../../base/common/observable.js'; import { IWorkspaceFolderCreationData } from '../../../../platform/workspaces/common/workspaces.js'; +import { getGitHubRemoteFileDisplayName } from '../../fileTreeView/browser/githubFileSystemProvider.js'; export class WorkspaceFolderManagementContribution extends Disposable implements IWorkbenchContribution { @@ -73,11 +74,12 @@ export class WorkspaceFolderManagementContribution extends Disposable implements if (session.providerType === AgentSessionProviders.Background) { return { uri: session.repository }; } - // if (session.providerType === AgentSessionProviders.Cloud) { - // return { - // uri: session.repository - // }; - // } + if (session.providerType === AgentSessionProviders.Cloud) { + return { + uri: session.repository, + name: getGitHubRemoteFileDisplayName(session.repository), + }; + } } return undefined; @@ -100,4 +102,6 @@ export class WorkspaceFolderManagementContribution extends Disposable implements private isUriTrusted(uri: URI): boolean { return this.workspaceTrustManagementService.getTrustedUris().some(trustedUri => this.uriIdentityService.extUri.isEqual(trustedUri, uri)); } + + }