From 0f6badb5368af36bffcce0399f431145a97dde3a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 12:23:26 +0000 Subject: [PATCH 1/4] Initial plan From 3e859260d35a252899057699d9a0b135f3088e77 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 12:28:52 +0000 Subject: [PATCH 2/4] Initial plan for detecting multiple accounts Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- .../vscode.proposed.chatContextProvider.d.ts | 13 +- ...ode.proposed.chatParticipantAdditions.d.ts | 184 ++++++++++++++- ...scode.proposed.chatParticipantPrivate.d.ts | 89 ++----- .../vscode.proposed.chatSessionsProvider.d.ts | 217 ++++++++++++++++-- 4 files changed, 411 insertions(+), 92 deletions(-) diff --git a/src/@types/vscode.proposed.chatContextProvider.d.ts b/src/@types/vscode.proposed.chatContextProvider.d.ts index ee971b9adb..043e067be1 100644 --- a/src/@types/vscode.proposed.chatContextProvider.d.ts +++ b/src/@types/vscode.proposed.chatContextProvider.d.ts @@ -16,7 +16,10 @@ declare module 'vscode' { * Providers registered without a selector will not be called for resource-based context. * - Explicitly. These context items are shown as options when the user explicitly attaches context. * - * To ensure your extension is activated when chat context is requested, make sure to include the `onChatContextProvider:` activation event in your `package.json`. + * To ensure your extension is activated when chat context is requested, make sure to include the following activations events: + * - If your extension implements `provideWorkspaceChatContext` or `provideChatContextForResource`, find an activation event which is a good signal to activate. + * Ex: `onLanguage:`, `onWebviewPanel:`, etc.` + * - If your extension implements `provideChatContextExplicit`, your extension will be automatically activated when the user requests explicit context. * * @param selector Optional document selector to filter which resources the provider is called for. If omitted, the provider will only be called for explicit context requests. * @param id Unique identifier for the provider. @@ -49,7 +52,7 @@ declare module 'vscode' { value?: string; /** * An optional command that is executed when the context item is clicked. - * The original context item will be passed as an argument to the command. + * The original context item will be passed as the first argument to the command. */ command?: Command; } @@ -62,7 +65,11 @@ declare module 'vscode' { onDidChangeWorkspaceChatContext?: Event; /** - * Provide a list of chat context items to be included as workspace context for all chat sessions. + * TODO @API: should this be a separate provider interface? + * + * Provide a list of chat context items to be included as workspace context for all chat requests. + * This should be used very sparingly to avoid providing useless context and to avoid using up the context window. + * A good example use case is to provide information about which branch the user is working on in a source control context. * * @param token A cancellation token. */ diff --git a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts index aa7001a3d2..8ef3f9335e 100644 --- a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts @@ -65,6 +65,29 @@ declare module 'vscode' { constructor(uri: Uri, edits: NotebookEdit | NotebookEdit[]); } + /** + * Represents a file-level edit (creation, deletion, or rename). + */ + export interface ChatWorkspaceFileEdit { + /** + * The original file URI (undefined for new files). + */ + oldResource?: Uri; + + /** + * The new file URI (undefined for deleted files). + */ + newResource?: Uri; + } + + /** + * Represents a workspace edit containing file-level operations. + */ + export class ChatResponseWorkspaceEditPart { + edits: ChatWorkspaceFileEdit[]; + constructor(edits: ChatWorkspaceFileEdit[]); + } + export class ChatResponseConfirmationPart { title: string; message: string | MarkdownString; @@ -80,9 +103,12 @@ declare module 'vscode' { constructor(value: Uri, license: string, snippet: string); } - export class ChatPrepareToolInvocationPart { - toolName: string; - constructor(toolName: string); + export interface ChatToolInvocationStreamData { + /** + * Partial or not-yet-validated arguments that have streamed from the language model. + * Tools may use this to render interim UI while the full invocation input is collected. + */ + readonly partialInput?: unknown; } export interface ChatTerminalToolInvocationData { @@ -92,6 +118,48 @@ declare module 'vscode' { toolEdited?: string; }; language: string; + + /** + * Terminal command output. Displayed when the terminal is no longer available. + */ + output?: { + /** The raw output text, may include ANSI escape codes. */ + text: string; + }; + + /** + * Command execution state. + */ + state?: { + /** Exit code of the command. */ + exitCode?: number; + /** Duration of execution in milliseconds. */ + duration?: number; + }; + } + + export class McpToolInvocationContentData { + /** + * The mime type which determines how the data property is interpreted. + */ + mimeType: string; + + /** + * The byte data for this part. + */ + data: Uint8Array; + + /** + * Construct a generic data part with the given content. + * @param data The byte data for this part. + * @param mimeType The mime type of the data. + */ + constructor(data: Uint8Array, mimeType: string); + } + + export interface ChatMcpToolInvocationData { + input: string; + output: McpToolInvocationContentData[]; } export class ChatToolInvocationPart { @@ -103,8 +171,8 @@ declare module 'vscode' { pastTenseMessage?: string | MarkdownString; isConfirmed?: boolean; isComplete?: boolean; - toolSpecificData?: ChatTerminalToolInvocationData; - fromSubAgent?: boolean; + toolSpecificData?: ChatTerminalToolInvocationData | ChatMcpToolInvocationData; + subAgentInvocationId?: string; presentation?: 'hidden' | 'hiddenAfterComplete' | undefined; constructor(toolName: string, toolCallId: string, isError?: boolean); @@ -176,7 +244,7 @@ declare module 'vscode' { constructor(uris: Uri[], callback: () => Thenable); } - export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseNotebookEditPart | ChatResponseConfirmationPart | ChatResponseCodeCitationPart | ChatResponseReferencePart2 | ChatResponseMovePart | ChatResponseExtensionsPart | ChatResponsePullRequestPart | ChatPrepareToolInvocationPart | ChatToolInvocationPart | ChatResponseMultiDiffPart | ChatResponseThinkingProgressPart | ChatResponseExternalEditPart; + export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseNotebookEditPart | ChatResponseWorkspaceEditPart | ChatResponseConfirmationPart | ChatResponseCodeCitationPart | ChatResponseReferencePart2 | ChatResponseMovePart | ChatResponseExtensionsPart | ChatResponsePullRequestPart | ChatToolInvocationPart | ChatResponseMultiDiffPart | ChatResponseThinkingProgressPart | ChatResponseExternalEditPart; export class ChatResponseWarningPart { value: MarkdownString; constructor(value: string | MarkdownString); @@ -310,6 +378,12 @@ declare module 'vscode' { notebookEdit(target: Uri, isDone: true): void; + /** + * Push a workspace edit containing file-level operations (create, delete, rename). + * @param edits Array of file-level edits to apply + */ + workspaceEdit(edits: ChatWorkspaceFileEdit[]): void; + /** * Makes an external edit to one or more resources. Changes to the * resources made within the `callback` and before it resolves will be @@ -349,7 +423,21 @@ declare module 'vscode' { codeCitation(value: Uri, license: string, snippet: string): void; - prepareToolInvocation(toolName: string): void; + /** + * Begin a tool invocation in streaming mode. This creates a tool invocation that will + * display streaming progress UI until the tool is actually invoked. + * @param toolCallId Unique identifier for this tool call, used to correlate streaming updates and final invocation. + * @param toolName The name of the tool being invoked. + * @param streamData Optional initial streaming data with partial arguments. + */ + beginToolInvocation(toolCallId: string, toolName: string, streamData?: ChatToolInvocationStreamData & { subagentInvocationId?: string }): void; + + /** + * Update the streaming data for a tool invocation that was started with `beginToolInvocation`. + * @param toolCallId The tool call ID that was passed to `beginToolInvocation`. + * @param streamData New streaming data with updated partial arguments. + */ + updateToolInvocation(toolCallId: string, streamData: ChatToolInvocationStreamData): void; push(part: ExtendedChatResponsePart): void; @@ -404,7 +492,7 @@ declare module 'vscode' { /** * A map of all tools that should (`true`) and should not (`false`) be used in this request. */ - readonly tools: Map; + readonly tools: Map; } export namespace lm { @@ -494,6 +582,47 @@ declare module 'vscode' { export type ChatExtendedRequestHandler = (request: ChatRequest, context: ChatContext, response: ChatResponseStream, token: CancellationToken) => ProviderResult; + /** + * Details about the prompt token usage by category and label. + */ + export interface ChatResultPromptTokenDetail { + /** + * The category this token usage belongs to (e.g., "System", "Context", "Conversation"). + */ + readonly category: string; + + /** + * The label for this specific token usage (e.g., "System prompt", "Attached files"). + */ + readonly label: string; + + /** + * The percentage of the total prompt tokens this represents (0-100). + */ + readonly percentageOfPrompt: number; + } + + /** + * Token usage information for a chat request. + */ + export interface ChatResultUsage { + /** + * The number of prompt tokens used in this request. + */ + readonly promptTokens: number; + + /** + * The number of completion tokens generated in this response. + */ + readonly completionTokens: number; + + /** + * Optional breakdown of prompt token usage by category and label. + * If the percentages do not sum to 100%, the remaining will be shown as "Uncategorized". + */ + readonly promptTokenDetails?: readonly ChatResultPromptTokenDetail[]; + } + export interface ChatResult { nextQuestion?: { prompt: string; @@ -504,6 +633,12 @@ declare module 'vscode' { * An optional detail string that will be rendered at the end of the response in certain UI contexts. */ details?: string; + + /** + * Token usage information for this request, if available. + * This is typically provided by the underlying language model. + */ + readonly usage?: ChatResultUsage; } export namespace chat { @@ -668,6 +803,39 @@ declare module 'vscode' { export interface LanguageModelToolInvocationOptions { model?: LanguageModelChat; + chatStreamToolCallId?: string; + } + + export interface LanguageModelToolInvocationStreamOptions { + /** + * Raw argument payload, such as the streamed JSON fragment from the language model. + */ + readonly rawInput?: unknown; + + readonly chatRequestId?: string; + /** @deprecated Use {@link chatSessionResource} instead */ + readonly chatSessionId?: string; + readonly chatSessionResource?: Uri; + readonly chatInteractionId?: string; + } + + export interface LanguageModelToolStreamResult { + /** + * A customized progress message to show while the tool runs. + */ + invocationMessage?: string | MarkdownString; + } + + export interface LanguageModelTool { + /** + * Called zero or more times before {@link LanguageModelTool.prepareInvocation} while the + * language model streams argument data for the invocation. Use this to update progress + * or UI with the partial arguments that have been generated so far. + * + * Implementations must be free of side-effects and should be resilient to receiving + * malformed or incomplete input. + */ + handleToolStream?(options: LanguageModelToolInvocationStreamOptions, token: CancellationToken): ProviderResult; } export interface ChatRequest { diff --git a/src/@types/vscode.proposed.chatParticipantPrivate.d.ts b/src/@types/vscode.proposed.chatParticipantPrivate.d.ts index c4a40a62c0..4ab722c122 100644 --- a/src/@types/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/@types/vscode.proposed.chatParticipantPrivate.d.ts @@ -61,10 +61,17 @@ declare module 'vscode' { readonly attempt: number; /** - * The session identifier for this chat request + * The session identifier for this chat request. + * + * @deprecated Use {@link chatSessionResource} instead. */ readonly sessionId: string; + /** + * The resource URI for the chat session this request belongs to. + */ + readonly sessionResource: Uri; + /** * If automatic command detection is enabled. */ @@ -93,7 +100,16 @@ declare module 'vscode' { */ readonly editedFileEvents?: ChatRequestEditedFileEvent[]; - readonly isSubagent?: boolean; + /** + * Unique ID for the subagent invocation, used to group tool calls from the same subagent run together. + * Pass this to tool invocations when calling tools from within a subagent context. + */ + readonly subAgentInvocationId?: string; + + /** + * Display name of the subagent that is invoking this request. + */ + readonly subAgentName?: string; } export enum ChatRequestEditedFileEventKind { @@ -230,13 +246,15 @@ declare module 'vscode' { export interface LanguageModelToolInvocationOptions { chatRequestId?: string; + /** @deprecated Use {@link chatSessionResource} instead */ chatSessionId?: string; + chatSessionResource?: Uri; chatInteractionId?: string; terminalCommand?: string; /** - * Lets us add some nicer UI to toolcalls that came from a sub-agent, but in the long run, this should probably just be rendered in a similar way to thinking text + tool call groups + * Unique ID for the subagent invocation, used to group tool calls from the same subagent run together. */ - fromSubAgent?: boolean; + subAgentInvocationId?: string; } export interface LanguageModelToolInvocationPrepareOptions { @@ -245,7 +263,9 @@ declare module 'vscode' { */ input: T; chatRequestId?: string; + /** @deprecated Use {@link chatSessionResource} instead */ chatSessionId?: string; + chatSessionResource?: Uri; chatInteractionId?: string; } @@ -319,65 +339,4 @@ declare module 'vscode' { } // #endregion - - // #region CustomAgentsProvider - - /** - * Represents a custom agent resource file (e.g., .agent.md or .prompt.md) available for a repository. - */ - export interface CustomAgentResource { - /** - * The unique identifier/name of the custom agent resource. - */ - readonly name: string; - - /** - * A description of what the custom agent resource does. - */ - readonly description: string; - - /** - * The URI to the agent or prompt resource file. - */ - readonly uri: Uri; - - /** - * Indicates whether the custom agent resource is editable. Defaults to false. - */ - readonly isEditable?: boolean; - } - - /** - * Options for querying custom agents. - */ - export interface CustomAgentQueryOptions { } - - /** - * A provider that supplies custom agent resources (from .agent.md and .prompt.md files) for repositories. - */ - export interface CustomAgentsProvider { - /** - * An optional event to signal that custom agents have changed. - */ - readonly onDidChangeCustomAgents?: Event; - - /** - * Provide the list of custom agent resources available for a given repository. - * @param options Optional query parameters. - * @param token A cancellation token. - * @returns An array of custom agent resources or a promise that resolves to such. - */ - provideCustomAgents(options: CustomAgentQueryOptions, token: CancellationToken): ProviderResult; - } - - export namespace chat { - /** - * Register a provider for custom agents. - * @param provider The custom agents provider. - * @returns A disposable that unregisters the provider when disposed. - */ - export function registerCustomAgentsProvider(provider: CustomAgentsProvider): Disposable; - } - - // #endregion } diff --git a/src/@types/vscode.proposed.chatSessionsProvider.d.ts b/src/@types/vscode.proposed.chatSessionsProvider.d.ts index 2ec68c1731..84cd547599 100644 --- a/src/@types/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/@types/vscode.proposed.chatSessionsProvider.d.ts @@ -26,6 +26,25 @@ declare module 'vscode' { InProgress = 2 } + export namespace chat { + /** + * Registers a new {@link ChatSessionItemProvider chat session item provider}. + * + * To use this, also make sure to also add `chatSessions` contribution in the `package.json`. + * + * @param chatSessionType The type of chat session the provider is for. + * @param provider The provider to register. + * + * @returns A disposable that unregisters the provider when disposed. + */ + export function registerChatSessionItemProvider(chatSessionType: string, provider: ChatSessionItemProvider): Disposable; + + /** + * Creates a new {@link ChatSessionItemController chat session item controller} with the given unique identifier. + */ + export function createChatSessionItemController(id: string, refreshHandler: () => Thenable): ChatSessionItemController; + } + /** * Provides a list of information about chat sessions. */ @@ -52,6 +71,84 @@ declare module 'vscode' { // #endregion } + /** + * Provides a list of information about chat sessions. + */ + export interface ChatSessionItemController { + readonly id: string; + + /** + * Unregisters the controller, disposing of its associated chat session items. + */ + dispose(): void; + + /** + * Managed collection of chat session items + */ + readonly items: ChatSessionItemCollection; + + /** + * Creates a new managed chat session item that be added to the collection. + */ + createChatSessionItem(resource: Uri, label: string): ChatSessionItem; + + /** + * Handler called to refresh the collection of chat session items. + * + * This is also called on first load to get the initial set of items. + */ + refreshHandler: () => Thenable; + + /** + * Fired when an item's archived state changes. + */ + readonly onDidChangeChatSessionItemState: Event; + } + + /** + * A collection of chat session items. It provides operations for managing and iterating over the items. + */ + export interface ChatSessionItemCollection extends Iterable { + /** + * Gets the number of items in the collection. + */ + readonly size: number; + + /** + * Replaces the items stored by the collection. + * @param items Items to store. + */ + replace(items: readonly ChatSessionItem[]): void; + + /** + * Iterate over each entry in this collection. + * + * @param callback Function to execute for each entry. + * @param thisArg The `this` context used when invoking the handler function. + */ + forEach(callback: (item: ChatSessionItem, collection: ChatSessionItemCollection) => unknown, thisArg?: any): void; + + /** + * Adds the chat session item to the collection. If an item with the same resource URI already + * exists, it'll be replaced. + * @param item Item to add. + */ + add(item: ChatSessionItem): void; + + /** + * Removes a single chat session item from the collection. + * @param resource Item resource to delete. + */ + delete(resource: Uri): void; + + /** + * Efficiently gets a chat session item by resource, if it exists, in the collection. + * @param resource Item resource to get. + * @returns The found item or undefined if it does not exist. + */ + get(resource: Uri): ChatSessionItem | undefined; + } + export interface ChatSessionItem { /** * The resource associated with the chat session. @@ -91,15 +188,42 @@ declare module 'vscode' { tooltip?: string | MarkdownString; /** - * The times at which session started and ended + * Whether the chat session has been archived. + */ + archived?: boolean; + + /** + * Timing information for the chat session */ timing?: { + /** + * Timestamp when the session was created in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + */ + created: number; + + /** + * Timestamp when the most recent request started in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + * + * Should be undefined if no requests have been made yet. + */ + lastRequestStarted?: number; + + /** + * Timestamp when the most recent request completed in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + * + * Should be undefined if the most recent request is still in progress or if no requests have been made yet. + */ + lastRequestEnded?: number; + /** * Session start timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + * @deprecated Use `created` and `lastRequestStarted` instead. */ - startTime: number; + startTime?: number; + /** * Session end timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + * @deprecated Use `lastRequestEnded` instead. */ endTime?: number; }; @@ -107,7 +231,7 @@ declare module 'vscode' { /** * Statistics about the chat session. */ - changes?: readonly ChatSessionChangedFile[] | { + changes?: readonly ChatSessionChangedFile[] | readonly ChatSessionChangedFile2[] | { /** * Number of files edited during the session. */ @@ -149,6 +273,35 @@ declare module 'vscode' { constructor(modifiedUri: Uri, insertions: number, deletions: number, originalUri?: Uri); } + export class ChatSessionChangedFile2 { + /** + * URI of the file. + */ + readonly uri: Uri; + + /** + * URI of the original file. Undefined if the file was created. + */ + readonly originalUri: Uri | undefined; + + /** + * URI of the modified file. Undefined if the file was deleted. + */ + readonly modifiedUri: Uri | undefined; + + /** + * Number of insertions made during the session. + */ + insertions: number; + + /** + * Number of deletions made during the session. + */ + deletions: number; + + constructor(uri: Uri, originalUri: Uri | undefined, modifiedUri: Uri | undefined, insertions: number, deletions: number); + } + export interface ChatSession { /** * The full history of the session @@ -252,7 +405,7 @@ declare module 'vscode' { * Called as soon as you register (call me once) * @param token */ - provideChatSessionProviderOptions?(token: CancellationToken): Thenable | ChatSessionProviderOptions; + provideChatSessionProviderOptions?(token: CancellationToken): Thenable; } export interface ChatSessionOptionUpdate { @@ -268,18 +421,6 @@ declare module 'vscode' { } export namespace chat { - /** - * Registers a new {@link ChatSessionItemProvider chat session item provider}. - * - * To use this, also make sure to also add `chatSessions` contribution in the `package.json`. - * - * @param chatSessionType The type of chat session the provider is for. - * @param provider The provider to register. - * - * @returns A disposable that unregisters the provider when disposed. - */ - export function registerChatSessionItemProvider(chatSessionType: string, provider: ChatSessionItemProvider): Disposable; - /** * Registers a new {@link ChatSessionContentProvider chat session content provider}. * @@ -337,6 +478,12 @@ declare module 'vscode' { * An icon for the option item shown in UI. */ readonly icon?: ThemeIcon; + + /** + * Indicates if this option should be selected by default. + * Only one item per option group should be marked as default. + */ + readonly default?: boolean; } /** @@ -362,6 +509,44 @@ declare module 'vscode' { * The selectable items within this option group. */ readonly items: ChatSessionProviderOptionItem[]; + + /** + * A context key expression that controls when this option group picker is visible. + * When specified, the picker is only shown when the expression evaluates to true. + * The expression can reference other option group values via `chatSessionOption.`. + * + * Example: `"chatSessionOption.models == 'gpt-4'"` - only show this picker when + * the 'models' option group has 'gpt-4' selected. + */ + readonly when?: string; + + /** + * When true, displays a searchable QuickPick with a "See more..." option. + * Recommended for option groups with additional async items (e.g., repositories). + */ + readonly searchable?: boolean; + + /** + * An icon for the option group shown in UI. + */ + readonly icon?: ThemeIcon; + + /** + * Handler for dynamic search when `searchable` is true. + * Called when the user types in the searchable QuickPick or clicks "See more..." to load additional items. + * + * @param query The search query entered by the user. Empty string for initial load. + * @param token A cancellation token. + * @returns Additional items to display in the searchable QuickPick. + */ + readonly onSearch?: (query: string, token: CancellationToken) => Thenable; + + /** + * Optional commands. + * + * These commands will be displayed at the bottom of the group. + */ + readonly commands?: Command[]; } export interface ChatSessionProviderOptions { From 1fd87767b4a6e99dd1e8232d5159877062a95697 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 12:33:42 +0000 Subject: [PATCH 3/4] Add detection for multiple accounts and suggestion to manage preferences Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/github/credentials.ts | 69 +++++++++++++++++++++++++++ src/github/folderRepositoryManager.ts | 15 ++++-- src/view/treeNodes/categoryNode.ts | 16 ++++++- 3 files changed, 94 insertions(+), 6 deletions(-) diff --git a/src/github/credentials.ts b/src/github/credentials.ts index ce2f62b102..3133f02d0c 100644 --- a/src/github/credentials.ts +++ b/src/github/credentials.ts @@ -454,6 +454,75 @@ export class CredentialStore extends Disposable { github.isEmu = getUser.then(result => result.data.plan?.name === 'emu_user'); } + /** + * Check if there are multiple GitHub accounts signed in to VS Code. + * This helps detect if the user might be using the wrong account for a repository. + */ + public async hasMultipleAccounts(authProviderId: AuthProvider): Promise { + try { + // Try different scope combinations to find all possible sessions + const scopesToCheck = [SCOPES_WITH_ADDITIONAL, SCOPES_OLD, SCOPES_OLDEST]; + const foundSessions = new Set(); + + for (const scopes of scopesToCheck) { + try { + const session = await vscode.authentication.getSession(authProviderId, scopes, { silent: true }); + if (session) { + foundSessions.add(session.account.id); + } + } catch { + // Ignore errors for individual scope checks + } + } + + // If we found sessions with different account IDs, there are multiple accounts + // However, the current API limitations mean we can only detect one session at a time + // So we use a different approach: check if there are accounts available to switch to + // by checking the account property on the session + + // For now, we'll assume if the user is authenticated, there might be multiple accounts + // The VS Code API doesn't easily expose all accounts, but the manage preferences command + // will show the user if they have multiple accounts configured + return foundSessions.size > 0; + } catch (e) { + Logger.error(`Error checking for multiple accounts: ${e}`, CredentialStore.ID); + return false; + } + } + + /** + * Show a modal dialog suggesting the user might be using the wrong GitHub account. + * Offers to open the "Manage Account Preferences" command. + * @param repoName The repository name that couldn't be accessed + * @param authProviderId The authentication provider ID + * @returns true if the user chose to manage account preferences, false otherwise + */ + public async showWrongAccountModal(repoName: string, authProviderId: AuthProvider): Promise { + const currentUser = await this.getCurrentUser(authProviderId); + const accountName = currentUser?.login ?? vscode.l10n.t('your current account'); + + const manageAccountPreferences = vscode.l10n.t('Manage Account Preferences'); + const result = await vscode.window.showErrorMessage( + vscode.l10n.t( + 'Unable to access repository "{0}" with the current GitHub account ({1}). You may have multiple GitHub accounts configured. Would you like to check your account preferences?', + repoName, + accountName + ), + { modal: true }, + manageAccountPreferences + ); + + if (result === manageAccountPreferences) { + try { + await vscode.commands.executeCommand('_account.manageAccountPreferences', 'GitHub.vscode-pull-request-github'); + } catch (e) { + Logger.error(`Failed to open manage account preferences: ${e}`, CredentialStore.ID); + } + return true; + } + return false; + } + private async getSession(authProviderId: AuthProvider, getAuthSessionOptions: vscode.AuthenticationGetSessionOptions, scopes: string[], requireScopes: boolean): Promise<{ session: vscode.AuthenticationSession | undefined, isNew: boolean, scopes: string[] }> { const existingSession = (getAuthSessionOptions.forceNewSession || requireScopes) ? undefined : await this.findExistingScopes(authProviderId); if (existingSession?.session) { diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index dac84ca592..0242f22135 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -1360,10 +1360,17 @@ export class FolderRepositoryManager extends Disposable { } catch (e) { Logger.error(`Fetching pull request with query failed: ${e}`, this.id); if (e.status === 404) { - // not found - vscode.window.showWarningMessage( - `Fetching pull requests for remote ${githubRepository.remote.remoteName} with query failed, please check if the repo ${githubRepository.remote.owner}/${githubRepository.remote.repositoryName} is valid.`, - ); + // not found - this might be due to using the wrong account + const repoName = `${githubRepository.remote.owner}/${githubRepository.remote.repositoryName}`; + const hasMultipleAccounts = await this._credentialStore.hasMultipleAccounts(githubRepository.remote.authProviderId); + if (hasMultipleAccounts) { + // Show modal suggesting the user might be using the wrong account + await this._credentialStore.showWrongAccountModal(repoName, githubRepository.remote.authProviderId); + } else { + vscode.window.showWarningMessage( + vscode.l10n.t('Fetching pull requests for remote {0} with query failed, please check if the repo {1} is valid.', githubRepository.remote.remoteName, repoName), + ); + } } else { throw e; } diff --git a/src/view/treeNodes/categoryNode.ts b/src/view/treeNodes/categoryNode.ts index b7f0d2baa2..d2e81298ed 100644 --- a/src/view/treeNodes/categoryNode.ts +++ b/src/view/treeNodes/categoryNode.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import { RemoteInfo } from '../../../common/types'; -import { AuthenticationError } from '../../common/authentication'; +import { AuthenticationError, AuthProvider } from '../../common/authentication'; import { DEV_MODE, PR_SETTINGS_NAMESPACE } from '../../common/settingKeys'; import { ITelemetry } from '../../common/telemetry'; import { toQueryUri } from '../../common/uri'; @@ -299,15 +299,27 @@ export class CategoryTreeNode extends TreeNode implements vscode.TreeItem { } catch (e) { if (this.isCopilot && (e.response.status === 422) && e.message.includes('the users do not exist')) { // Skip it, it's copilot and the repo doesn't have copilot + } else if (e.status === 404 || e.response?.status === 404) { + // 404 errors might indicate wrong account - this is handled in folderRepositoryManager + // but we catch it here to prevent duplicate error messages + needLogin = e instanceof AuthenticationError; } else { const error = formatError(e); const actions: string[] = []; if (error.includes('Bad credentials')) { actions.push(vscode.l10n.t('Login again')); + } else if (e.status === 403 || e.response?.status === 403) { + // 403 forbidden - user might not have access with current account + const hasMultipleAccounts = await this.folderRepoManager.credentialStore.hasMultipleAccounts(AuthProvider.github); + if (hasMultipleAccounts) { + actions.push(vscode.l10n.t('Check Account Preferences')); + } } vscode.window.showErrorMessage(vscode.l10n.t('Fetching pull requests failed: {0}', formatError(e)), ...actions).then(action => { - if (action && action === actions[0]) { + if (action === vscode.l10n.t('Login again')) { this.folderRepoManager.credentialStore.recreate(vscode.l10n.t('Your login session is no longer valid.')); + } else if (action === vscode.l10n.t('Check Account Preferences')) { + vscode.commands.executeCommand('_account.manageAccountPreferences', 'GitHub.vscode-pull-request-github'); } }); needLogin = e instanceof AuthenticationError; From ebd3e55edcf7f72bab4bf87bdde99a05485628d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 12:37:26 +0000 Subject: [PATCH 4/4] Address code review feedback: add error handling for getCurrentUser Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/github/credentials.ts | 47 ++++++++------------------- src/github/folderRepositoryManager.ts | 4 +-- src/view/treeNodes/categoryNode.ts | 6 ++-- 3 files changed, 19 insertions(+), 38 deletions(-) diff --git a/src/github/credentials.ts b/src/github/credentials.ts index 3133f02d0c..18469f845c 100644 --- a/src/github/credentials.ts +++ b/src/github/credentials.ts @@ -455,39 +455,13 @@ export class CredentialStore extends Disposable { } /** - * Check if there are multiple GitHub accounts signed in to VS Code. - * This helps detect if the user might be using the wrong account for a repository. + * Check if the user is authenticated and might benefit from checking account preferences. + * Note: Due to VS Code API limitations, we cannot directly check if multiple accounts are available. + * This method returns true if the user is authenticated, as they may have multiple accounts + * configured in VS Code that they can switch to via the "Manage Account Preferences" command. */ - public async hasMultipleAccounts(authProviderId: AuthProvider): Promise { - try { - // Try different scope combinations to find all possible sessions - const scopesToCheck = [SCOPES_WITH_ADDITIONAL, SCOPES_OLD, SCOPES_OLDEST]; - const foundSessions = new Set(); - - for (const scopes of scopesToCheck) { - try { - const session = await vscode.authentication.getSession(authProviderId, scopes, { silent: true }); - if (session) { - foundSessions.add(session.account.id); - } - } catch { - // Ignore errors for individual scope checks - } - } - - // If we found sessions with different account IDs, there are multiple accounts - // However, the current API limitations mean we can only detect one session at a time - // So we use a different approach: check if there are accounts available to switch to - // by checking the account property on the session - - // For now, we'll assume if the user is authenticated, there might be multiple accounts - // The VS Code API doesn't easily expose all accounts, but the manage preferences command - // will show the user if they have multiple accounts configured - return foundSessions.size > 0; - } catch (e) { - Logger.error(`Error checking for multiple accounts: ${e}`, CredentialStore.ID); - return false; - } + public async isAuthenticatedForAccountPreferences(authProviderId: AuthProvider): Promise { + return this.isAuthenticated(authProviderId); } /** @@ -498,8 +472,13 @@ export class CredentialStore extends Disposable { * @returns true if the user chose to manage account preferences, false otherwise */ public async showWrongAccountModal(repoName: string, authProviderId: AuthProvider): Promise { - const currentUser = await this.getCurrentUser(authProviderId); - const accountName = currentUser?.login ?? vscode.l10n.t('your current account'); + let accountName: string; + try { + const currentUser = await this.getCurrentUser(authProviderId); + accountName = currentUser?.login ?? vscode.l10n.t('your current account'); + } catch { + accountName = vscode.l10n.t('your current account'); + } const manageAccountPreferences = vscode.l10n.t('Manage Account Preferences'); const result = await vscode.window.showErrorMessage( diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index 0242f22135..0850bb3782 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -1362,8 +1362,8 @@ export class FolderRepositoryManager extends Disposable { if (e.status === 404) { // not found - this might be due to using the wrong account const repoName = `${githubRepository.remote.owner}/${githubRepository.remote.repositoryName}`; - const hasMultipleAccounts = await this._credentialStore.hasMultipleAccounts(githubRepository.remote.authProviderId); - if (hasMultipleAccounts) { + const isAuthenticated = await this._credentialStore.isAuthenticatedForAccountPreferences(githubRepository.remote.authProviderId); + if (isAuthenticated) { // Show modal suggesting the user might be using the wrong account await this._credentialStore.showWrongAccountModal(repoName, githubRepository.remote.authProviderId); } else { diff --git a/src/view/treeNodes/categoryNode.ts b/src/view/treeNodes/categoryNode.ts index d2e81298ed..00d289f519 100644 --- a/src/view/treeNodes/categoryNode.ts +++ b/src/view/treeNodes/categoryNode.ts @@ -310,8 +310,10 @@ export class CategoryTreeNode extends TreeNode implements vscode.TreeItem { actions.push(vscode.l10n.t('Login again')); } else if (e.status === 403 || e.response?.status === 403) { // 403 forbidden - user might not have access with current account - const hasMultipleAccounts = await this.folderRepoManager.credentialStore.hasMultipleAccounts(AuthProvider.github); - if (hasMultipleAccounts) { + // Check both GitHub.com and Enterprise providers since we might have repos from either + const isAuthenticatedGitHub = await this.folderRepoManager.credentialStore.isAuthenticatedForAccountPreferences(AuthProvider.github); + const isAuthenticatedEnterprise = await this.folderRepoManager.credentialStore.isAuthenticatedForAccountPreferences(AuthProvider.githubEnterprise); + if (isAuthenticatedGitHub || isAuthenticatedEnterprise) { actions.push(vscode.l10n.t('Check Account Preferences')); } }