Sessions window: GH file system provider fixes

This commit is contained in:
Osvaldo Ortega
2026-03-02 16:48:34 -08:00
parent 83601ca509
commit bec7b2ba7d
6 changed files with 110 additions and 20 deletions

View File

@@ -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<Change
const root: FolderNode = { name: '', uri: URI.file('/'), children: new Map(), files: [] };
for (const item of items) {
const dirPath = dirname(item.uri.path);
let dirPath = dirname(item.uri.path);
// For github-remote-file URIs, strip the /{owner}/{repo}/{ref} prefix
// so the tree shows repo-relative paths instead of internal URI segments.
if (item.uri.scheme === GITHUB_REMOTE_FILE_SCHEME) {
const parts = dirPath.split('/').filter(Boolean);
if (parts.length >= 3) {
dirPath = '/' + parts.slice(3).join('/');
} else {
dirPath = '/';
}
}
const segments = dirPath.split('/').filter(Boolean);
let current = root;

View File

@@ -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`));
}
}

View File

@@ -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;

View File

@@ -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<string, ITreeCacheEntry>();
/** Negative cache for refs that returned 404, keyed by "owner/repo/ref" */
private readonly notFoundCache = new Map<string, number>();
/** In-flight fetch promises keyed by "owner/repo/ref" to deduplicate concurrent requests */
private readonly pendingFetches = new Map<string, Promise<ITreeCacheEntry>>();
/** 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<string> {
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<ITreeCacheEntry> {
private fetchTree(owner: string, repo: string, ref: string): Promise<ITreeCacheEntry> {
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<ITreeCacheEntry> {
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<IGitHubTreeResponse>(response);
let data: IGitHubTreeResponse | null;
try {
data = await asJson<IGitHubTreeResponse>(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();
}
}

View File

@@ -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<boolean>('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;

View File

@@ -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));
}
}