mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-02 08:15:56 +01:00
Sessions window: GH file system provider fixes
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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`));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user