From c975069c241f7089fe3d2a858fd0a322ab344db1 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Wed, 25 Jun 2025 15:44:07 -0700 Subject: [PATCH 01/33] Adds support for mcp server installation --- contributions.json | 4 + package.json | 9 + src/constants.commands.generated.ts | 1 + src/constants.storage.ts | 3 + src/env/node/gk/cli/integration.ts | 362 +++++++++++++++++++++++++++- 5 files changed, 377 insertions(+), 2 deletions(-) diff --git a/contributions.json b/contributions.json index a478a4fd7742e..2a2402f598c25 100644 --- a/contributions.json +++ b/contributions.json @@ -311,6 +311,10 @@ "icon": "$(sparkle)", "commandPalette": "gitlens:enabled && !gitlens:untrusted && gitlens:gk:organization:ai:enabled" }, + "gitlens.ai.mcp.install": { + "label": "Install MCP Server", + "commandPalette": "gitlens:enabled && !gitlens:untrusted && gitlens:gk:organization:ai:enabled" + }, "gitlens.ai.rebaseOntoCommit:graph": { "label": "AI Rebase Current Branch onto Commit (Preview)...", "icon": "$(sparkle)", diff --git a/package.json b/package.json index 3329d75c53940..78e792c54391c 100644 --- a/package.json +++ b/package.json @@ -6296,6 +6296,11 @@ "category": "GitLens", "icon": "$(sparkle)" }, + { + "command": "gitlens.ai.mcp.install", + "title": "Install MCP Server", + "category": "GitLens" + }, { "command": "gitlens.ai.rebaseOntoCommit:graph", "title": "AI Rebase Current Branch onto Commit (Preview)...", @@ -11070,6 +11075,10 @@ "command": "gitlens.ai.generateRebase", "when": "gitlens:enabled && !gitlens:untrusted && gitlens:gk:organization:ai:enabled" }, + { + "command": "gitlens.ai.mcp.install", + "when": "gitlens:enabled && !gitlens:untrusted && gitlens:gk:organization:ai:enabled" + }, { "command": "gitlens.ai.rebaseOntoCommit:graph", "when": "false" diff --git a/src/constants.commands.generated.ts b/src/constants.commands.generated.ts index e73c08a405815..8b649ebe65f9e 100644 --- a/src/constants.commands.generated.ts +++ b/src/constants.commands.generated.ts @@ -690,6 +690,7 @@ export type ContributedPaletteCommands = | 'gitlens.ai.generateChangelog' | 'gitlens.ai.generateCommitMessage' | 'gitlens.ai.generateRebase' + | 'gitlens.ai.mcp.install' | 'gitlens.ai.switchProvider' | 'gitlens.applyPatchFromClipboard' | 'gitlens.associateIssueWithBranch' diff --git a/src/constants.storage.ts b/src/constants.storage.ts index 34de3726f0fd6..d2ffc10b040a1 100644 --- a/src/constants.storage.ts +++ b/src/constants.storage.ts @@ -63,6 +63,8 @@ export type DeprecatedGlobalStorage = { }; export type GlobalStorage = { + 'ai:mcp:attemptInstall': string; + 'ai:mcp:installPath': string; avatars: [string, StoredAvatar][]; 'confirm:ai:generateCommits': boolean; 'confirm:ai:generateRebase': boolean; @@ -83,6 +85,7 @@ export type GlobalStorage = { // Value based on `currentOnboardingVersion` in composer's protocol 'composer:onboarding:dismissed': string; 'composer:onboarding:stepReached': number; + 'gk:cli:installedPath': string; 'home:sections:collapsed': string[]; 'home:walkthrough:dismissed': boolean; 'launchpad:groups:collapsed': StoredLaunchpadGroup[]; diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index 150d197351aac..22e7b0febdd4c 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -1,7 +1,16 @@ +import { arch } from 'process'; import type { ConfigurationChangeEvent } from 'vscode'; -import { Disposable } from 'vscode'; +import { version as codeVersion, Disposable, env, ProgressLocation, Uri, window, workspace } from 'vscode'; import type { Container } from '../../../../container'; +import type { SubscriptionChangeEvent } from '../../../../plus/gk/subscriptionService'; +import { registerCommand } from '../../../../system/-webview/command'; import { configuration } from '../../../../system/-webview/configuration'; +import { getContext } from '../../../../system/-webview/context'; +import { openUrl } from '../../../../system/-webview/vscode/uris'; +import { Logger } from '../../../../system/logger'; +import { compare } from '../../../../system/version'; +import { run } from '../../git/shell'; +import { getPlatform, isWeb } from '../../platform'; import { CliCommandHandlers } from './commands'; import type { IpcServer } from './ipcServer'; import { createIpcServer } from './ipcServer'; @@ -18,9 +27,19 @@ export class GkCliIntegrationProvider implements Disposable { private _runningDisposable: Disposable | undefined; constructor(private readonly container: Container) { - this._disposable = configuration.onDidChange(e => this.onConfigurationChanged(e)); + this._disposable = Disposable.from( + configuration.onDidChange(e => this.onConfigurationChanged(e)), + this.container.subscription.onDidChange(this.onSubscriptionChanged, this), + ...this.registerCommands(), + ); this.onConfigurationChanged(); + setTimeout( + () => { + void this.installMCPIfNeeded(true); + }, + 10000 + Math.floor(Math.random() * 20000), + ); } dispose(): void { @@ -56,4 +75,343 @@ export class GkCliIntegrationProvider implements Disposable { this._runningDisposable?.dispose(); this._runningDisposable = undefined; } + + private async installMCPIfNeeded(silent?: boolean): Promise { + try { + if ( + (env.appName === 'Visual Studio Code' || env.appName === 'Visual Studio Code - Insiders') && + compare(codeVersion, '1.102') < 0 + ) { + if (!silent) { + void window.showInformationMessage('Use of this command requires VS Code 1.102 or later.'); + } + return; + } + + if (silent && this.container.storage.get('ai:mcp:attemptInstall')) { + return; + } + + void this.container.storage.store('ai:mcp:attemptInstall', 'attempted').catch(); + + if (configuration.get('ai.enabled') === false) { + const message = 'Cannot install MCP: AI is disabled in settings'; + Logger.log(message); + if (silent !== true) { + void window.showErrorMessage(message); + } + return; + } + + if (getContext('gitlens:gk:organization:ai:enabled', true) !== true) { + const message = 'Cannot install MCP: AI is disabled by your organization'; + Logger.log(message); + if (silent !== true) { + void window.showErrorMessage(message); + } + return; + } + + if (isWeb) { + const message = 'Cannot install MCP: web environment is not supported'; + Logger.log(message); + if (silent !== true) { + void window.showErrorMessage(message); + } + return; + } + + // Detect platform and architecture + const platform = getPlatform(); + + // Map platform names for the API and get architecture + let platformName: string; + let architecture: string; + + switch (arch) { + case 'x64': + architecture = 'x64'; + break; + case 'arm64': + architecture = 'arm64'; + break; + default: + architecture = 'x86'; // Default to x86 for other architectures + break; + } + + switch (platform) { + case 'windows': + platformName = 'windows'; + break; + case 'macOS': + platformName = 'darwin'; + break; + case 'linux': + platformName = 'linux'; + break; + default: { + const message = `Skipping MCP installation: unsupported platform ${platform}`; + Logger.log(message); + if (silent !== true) { + void window.showErrorMessage( + `Cannot install MCP integration: unsupported platform ${platform}`, + ); + } + return; + } + } + + const mcpFileName = platform === 'windows' ? 'gk.exe' : 'gk'; + + // Wrap the main installation process with progress indicator if not silent + const installationTask = async () => { + let mcpInstallerPath: Uri | undefined; + let mcpExtractedFolderPath: Uri | undefined; + let mcpExtractedPath: Uri | undefined; + + try { + // Download the MCP proxy installer + const proxyUrl = this.container.urls.getGkApiUrl( + 'releases', + 'gkcli-proxy', + 'production', + platformName, + architecture, + 'active', + ); + + let response = await fetch(proxyUrl); + if (!response.ok) { + const errorMsg = `Failed to get MCP installer proxy: ${response.status} ${response.statusText}`; + Logger.error(errorMsg); + throw new Error(errorMsg); + } + + let downloadUrl: string | undefined; + try { + const mcpInstallerInfo: { version?: string; packages?: { zip?: string } } | undefined = + (await response.json()) as any; + downloadUrl = mcpInstallerInfo?.packages?.zip; + } catch (ex) { + const errorMsg = `Failed to parse MCP installer info: ${ex}`; + Logger.error(errorMsg); + throw new Error(errorMsg); + } + + if (downloadUrl == null) { + const errorMsg = 'Failed to find download URL for MCP proxy installer'; + Logger.error(errorMsg); + throw new Error(errorMsg); + } + + response = await fetch(downloadUrl); + if (!response.ok) { + const errorMsg = `Failed to download MCP proxy installer: ${response.status} ${response.statusText}`; + Logger.error(errorMsg); + throw new Error(errorMsg); + } + + const installerData = await response.arrayBuffer(); + if (installerData.byteLength === 0) { + const errorMsg = 'Downloaded installer is empty'; + Logger.error(errorMsg); + throw new Error(errorMsg); + } + // installer file name is the last part of the download URL + const installerFileName = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1); + mcpInstallerPath = Uri.joinPath(this.container.context.globalStorageUri, installerFileName); + + // Ensure the global storage directory exists + await workspace.fs.createDirectory(this.container.context.globalStorageUri); + + // Write the installer to the extension storage + await workspace.fs.writeFile(mcpInstallerPath, new Uint8Array(installerData)); + Logger.log(`Downloaded MCP proxy installer successfully`); + + try { + // Use the run function to extract the installer file from the installer zip + if (platform === 'windows') { + // On Windows, use PowerShell to extract the zip file + await run( + 'powershell.exe', + [ + '-Command', + `Expand-Archive -Path "${mcpInstallerPath.fsPath}" -DestinationPath "${this.container.context.globalStorageUri.fsPath}"`, + ], + 'utf8', + ); + } else { + // On Unix-like systems, use the unzip command to extract the zip file + await run( + 'unzip', + ['-o', mcpInstallerPath.fsPath, '-d', this.container.context.globalStorageUri.fsPath], + 'utf8', + ); + } + // The gk file should be in a subfolder named after the installer file name + const extractedFolderName = installerFileName.replace(/\.zip$/, ''); + mcpExtractedFolderPath = Uri.joinPath( + this.container.context.globalStorageUri, + extractedFolderName, + ); + mcpExtractedPath = Uri.joinPath(mcpExtractedFolderPath, mcpFileName); + + // Check using stat to make sure the newly extracted file exists. + await workspace.fs.stat(mcpExtractedPath); + void this.container.storage.store('ai:mcp:installPath', mcpExtractedFolderPath.fsPath).catch(); + } catch (error) { + const errorMsg = `Failed to extract MCP installer: ${error}`; + Logger.error(errorMsg); + throw new Error(errorMsg); + } + + // Get the app name + let appName = 'vscode'; + let isInsiders = false; + switch (env.appName) { + case 'Visual Studio Code': + break; + case 'Visual Studio Code - Insiders': + isInsiders = true; + break; + case 'Cursor': + appName = 'cursor'; + break; + case 'Windsurf': + appName = 'windsurf'; + break; + default: { + const errorMsg = `Failed to install MCP: unsupported app name - ${env.appName}`; + Logger.error(errorMsg); + throw new Error(errorMsg); + } + } + + // Configure the MCP server in settings.json + try { + const installOutput = await run( + platform === 'windows' ? mcpFileName : `./${mcpFileName}`, + ['install'], + 'utf8', + { cwd: mcpExtractedFolderPath.fsPath }, + ); + const directory = installOutput.match(/Directory: (.*)/); + let directoryPath; + if (directory != null && directory.length > 1) { + directoryPath = directory[1]; + void this.container.storage.store('gk:cli:installedPath', directoryPath).catch(); + } else { + Logger.warn('MCP Install: Failed to find directory in install output'); + if (appName === 'vscode') { + throw new Error('MCP command path not availavle'); + } + } + + if (appName === 'vscode') { + const config = { + name: 'GitKraken', + command: mcpExtractedPath.fsPath, + args: ['mcp'], + type: 'stdio', + }; + const installDeepLinkUrl = `${isInsiders ? 'vscode-insiders' : 'vscode'}:mcp/install?${encodeURIComponent(JSON.stringify(config))}`; + await openUrl(installDeepLinkUrl); + } else { + await run( + platform === 'windows' ? mcpFileName : `./${mcpFileName}`, + ['mcp', 'install', appName], + 'utf8', + { cwd: mcpExtractedFolderPath.fsPath }, + ); + } + + const gkAuth = (await this.container.subscription.getAuthenticationSession())?.accessToken; + if (gkAuth != null) { + await run( + platform === 'windows' ? mcpFileName : `./${mcpFileName}`, + ['auth', 'login', '-t', gkAuth], + 'utf8', + { cwd: mcpExtractedFolderPath.fsPath }, + ); + } + + Logger.log( + `MCP configuration completed.${appName === 'vscode' ? " Click 'install' to install." : ''}`, + ); + } catch (error) { + const errorMsg = `MCP server configuration failed: ${error}`; + Logger.error(errorMsg); + throw new Error(errorMsg); + } + } finally { + if (mcpInstallerPath != null) { + try { + await workspace.fs.delete(mcpInstallerPath); + } catch (error) { + Logger.warn(`Failed to delete MCP installer zip file: ${error}`); + } + } + } + }; + + // Execute the installation task with or without progress indicator + if (silent !== true) { + await window.withProgress( + { + location: ProgressLocation.Notification, + title: 'Installing MCP integration...', + cancellable: false, + }, + async () => { + await installationTask(); + }, + ); + } else { + await installationTask(); + } + + // Show success notification if not silent + void this.container.storage.store('ai:mcp:attemptInstall', 'completed').catch(); + void window.showInformationMessage('GitKraken MCP integration installed successfully'); + } catch (error) { + Logger.error(`Error during MCP installation: ${error}`); + + // Show error notification if not silent + if (silent !== true) { + void window.showErrorMessage( + `Failed to install MCP integration: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + } + + private async onSubscriptionChanged(e: SubscriptionChangeEvent): Promise { + const mcpInstallStatus = this.container.storage.get('ai:mcp:attemptInstall'); + const mcpInstallPath = this.container.storage.get('ai:mcp:installPath'); + + const platform = getPlatform(); + if ( + e.current?.account?.id != null && + e.current.account.id !== e.previous?.account?.id && + mcpInstallStatus === 'completed' && + mcpInstallPath != null + ) { + const currentSessionToken = (await this.container.subscription.getAuthenticationSession())?.accessToken; + if (currentSessionToken != null) { + try { + await run( + platform === 'windows' ? 'gk.exe' : './gk', + ['auth', 'login', '-t', currentSessionToken], + 'utf8', + { cwd: mcpInstallPath }, + ); + } catch {} + } + } + } + + private registerCommands(): Disposable[] { + return [registerCommand('gitlens.ai.mcp.install', () => this.installMCPIfNeeded())]; + } } From 68d4130b7e62e2c39551452e6f8a51a612393200 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Mon, 25 Aug 2025 10:28:47 -0700 Subject: [PATCH 02/33] Updates storage key and splits automatic and manual installation subtasks --- src/constants.storage.ts | 2 +- src/env/node/gk/cli/integration.ts | 381 +++++++++++++++-------------- 2 files changed, 197 insertions(+), 186 deletions(-) diff --git a/src/constants.storage.ts b/src/constants.storage.ts index d2ffc10b040a1..634332eecde74 100644 --- a/src/constants.storage.ts +++ b/src/constants.storage.ts @@ -63,7 +63,7 @@ export type DeprecatedGlobalStorage = { }; export type GlobalStorage = { - 'ai:mcp:attemptInstall': string; + 'ai:mcp:install': string; 'ai:mcp:installPath': string; avatars: [string, StoredAvatar][]; 'confirm:ai:generateCommits': boolean; diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index 22e7b0febdd4c..7a0e3589db6ef 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -8,6 +8,7 @@ import { configuration } from '../../../../system/-webview/configuration'; import { getContext } from '../../../../system/-webview/context'; import { openUrl } from '../../../../system/-webview/vscode/uris'; import { Logger } from '../../../../system/logger'; +import { getLogScope } from '../../../../system/logger.scope'; import { compare } from '../../../../system/version'; import { run } from '../../git/shell'; import { getPlatform, isWeb } from '../../platform'; @@ -34,12 +35,11 @@ export class GkCliIntegrationProvider implements Disposable { ); this.onConfigurationChanged(); - setTimeout( - () => { - void this.installMCPIfNeeded(true); - }, - 10000 + Math.floor(Math.random() * 20000), - ); + + const mcpInstallStatus = this.container.storage.get('ai:mcp:install'); + if (!mcpInstallStatus) { + setTimeout(() => this.setupMCPInstallation(true), 10000 + Math.floor(Math.random() * 20000)); + } } dispose(): void { @@ -76,49 +76,124 @@ export class GkCliIntegrationProvider implements Disposable { this._runningDisposable = undefined; } - private async installMCPIfNeeded(silent?: boolean): Promise { + private async installMCP(): Promise { + const scope = getLogScope(); try { if ( (env.appName === 'Visual Studio Code' || env.appName === 'Visual Studio Code - Insiders') && compare(codeVersion, '1.102') < 0 ) { - if (!silent) { - void window.showInformationMessage('Use of this command requires VS Code 1.102 or later.'); + void window.showInformationMessage('Use of this command requires VS Code 1.102 or later.'); + return; + } + + let appName = 'vscode'; + let isInsiders = false; + switch (env.appName) { + case 'Visual Studio Code': + break; + case 'Visual Studio Code - Insiders': + isInsiders = true; + break; + case 'Cursor': + appName = 'cursor'; + break; + case 'Windsurf': + appName = 'windsurf'; + break; + default: { + void window.showInformationMessage(`MCP installation is not supported for app: ${env.appName}`); + return; + } + } + + let autoInstallProgress = this.container.storage.get('ai:mcp:install'); + let mcpPath = this.container.storage.get('ai:mcp:installPath'); + let mcpFileExists = true; + if (mcpPath != null) { + try { + await workspace.fs.stat( + Uri.joinPath(Uri.file(mcpPath), getPlatform() === 'windows' ? 'gk.exe' : 'gk'), + ); + } catch { + mcpFileExists = false; + } + } + if (autoInstallProgress !== 'completed' || mcpPath == null || !mcpFileExists) { + await this.setupMCPInstallation(); + } + + autoInstallProgress = this.container.storage.get('ai:mcp:install'); + mcpPath = this.container.storage.get('ai:mcp:installPath'); + if (autoInstallProgress !== 'completed' || mcpPath == null) { + void window.showErrorMessage('Failed to install MCP integration: setup failed to complete.'); + return; + } + + // TODO: REMOVE THIS ONCE VSCODE-INSIDERS IS ADDED AS AN OFFICIAL PROVIDER TO MCP INSTALL COMMAND + if (appName === 'vscode' && isInsiders) { + const mcpFileName = getPlatform() === 'windows' ? 'gk.exe' : 'gk'; + const mcpProxyPath = Uri.joinPath(Uri.file(mcpPath), mcpFileName); + const config = { + name: 'GitKraken', + command: mcpProxyPath.fsPath, + args: ['mcp'], + type: 'stdio', + }; + const installDeepLinkUrl = `vscode-insiders:mcp/install?${encodeURIComponent(JSON.stringify(config))}`; + await openUrl(installDeepLinkUrl); + } else { + if (appName !== 'cursor' && appName !== 'vscode') { + const confirmation = await window.showInformationMessage( + `MCP configured successfully. Click 'Finish' to add it to your MCP server list and complete the installation.`, + { modal: true }, + { title: 'Finish' }, + { title: 'Cancel', isCloseAffordance: true }, + ); + if (confirmation == null || confirmation.title === 'Cancel') return; } + + const _output = await this.runMcpCommand(['mcp', 'install', appName, '--source=gitlens'], { + cwd: mcpPath, + }); + // TODO: GET THE INSTALL LINK FROM THE OUTPUT IF IT EXISTS AND OPEN IT. + // CURRENTLY THE CLI TRIES TO DO SO BUT THE LINK DOES NOT WORK SINCE IT IS IN THE CHILD PROCESS. + } + } catch (ex) { + Logger.error(`Error during MCP installation: ${ex}`, scope); + + void window.showErrorMessage( + `Failed to install MCP integration: ${ex instanceof Error ? ex.message : String(ex)}`, + ); + } + } + + private async setupMCPInstallation(autoInstall?: boolean): Promise { + try { + if ( + (env.appName === 'Visual Studio Code' || env.appName === 'Visual Studio Code - Insiders') && + compare(codeVersion, '1.102') < 0 + ) { return; } - if (silent && this.container.storage.get('ai:mcp:attemptInstall')) { + // Kick out early if we already attempted an auto-install + if (autoInstall && this.container.storage.get('ai:mcp:install')) { return; } - void this.container.storage.store('ai:mcp:attemptInstall', 'attempted').catch(); + void this.container.storage.store('ai:mcp:install', 'attempted').catch(); if (configuration.get('ai.enabled') === false) { - const message = 'Cannot install MCP: AI is disabled in settings'; - Logger.log(message); - if (silent !== true) { - void window.showErrorMessage(message); - } - return; + throw new Error('AI is disabled in settings'); } if (getContext('gitlens:gk:organization:ai:enabled', true) !== true) { - const message = 'Cannot install MCP: AI is disabled by your organization'; - Logger.log(message); - if (silent !== true) { - void window.showErrorMessage(message); - } - return; + throw new Error('AI is disabled by your organization'); } if (isWeb) { - const message = 'Cannot install MCP: web environment is not supported'; - Logger.log(message); - if (silent !== true) { - void window.showErrorMessage(message); - } - return; + throw new Error('Web environment is not supported'); } // Detect platform and architecture @@ -151,24 +226,15 @@ export class GkCliIntegrationProvider implements Disposable { platformName = 'linux'; break; default: { - const message = `Skipping MCP installation: unsupported platform ${platform}`; - Logger.log(message); - if (silent !== true) { - void window.showErrorMessage( - `Cannot install MCP integration: unsupported platform ${platform}`, - ); - } - return; + throw new Error(`Unsupported platform ${platform}`); } } - const mcpFileName = platform === 'windows' ? 'gk.exe' : 'gk'; - // Wrap the main installation process with progress indicator if not silent const installationTask = async () => { let mcpInstallerPath: Uri | undefined; - let mcpExtractedFolderPath: Uri | undefined; - let mcpExtractedPath: Uri | undefined; + let mcpExtractedFilePath: Uri | undefined; + const mcpFolderPath = this.container.context.globalStorageUri; try { // Download the MCP proxy installer @@ -183,9 +249,7 @@ export class GkCliIntegrationProvider implements Disposable { let response = await fetch(proxyUrl); if (!response.ok) { - const errorMsg = `Failed to get MCP installer proxy: ${response.status} ${response.statusText}`; - Logger.error(errorMsg); - throw new Error(errorMsg); + throw new Error(`Failed to get MCP installer info: ${response.status} ${response.statusText}`); } let downloadUrl: string | undefined; @@ -194,173 +258,111 @@ export class GkCliIntegrationProvider implements Disposable { (await response.json()) as any; downloadUrl = mcpInstallerInfo?.packages?.zip; } catch (ex) { - const errorMsg = `Failed to parse MCP installer info: ${ex}`; - Logger.error(errorMsg); - throw new Error(errorMsg); + throw new Error(`Failed to parse MCP installer info: ${ex}`); } if (downloadUrl == null) { - const errorMsg = 'Failed to find download URL for MCP proxy installer'; - Logger.error(errorMsg); - throw new Error(errorMsg); + throw new Error('Failed to find download URL for MCP proxy installer'); } response = await fetch(downloadUrl); if (!response.ok) { - const errorMsg = `Failed to download MCP proxy installer: ${response.status} ${response.statusText}`; - Logger.error(errorMsg); - throw new Error(errorMsg); + throw new Error( + `Failed to fetch MCP proxy installer: ${response.status} ${response.statusText}`, + ); } const installerData = await response.arrayBuffer(); if (installerData.byteLength === 0) { - const errorMsg = 'Downloaded installer is empty'; - Logger.error(errorMsg); - throw new Error(errorMsg); + throw new Error('Fetched MCP installer is empty'); } // installer file name is the last part of the download URL const installerFileName = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1); - mcpInstallerPath = Uri.joinPath(this.container.context.globalStorageUri, installerFileName); + mcpInstallerPath = Uri.joinPath(mcpFolderPath, installerFileName); // Ensure the global storage directory exists - await workspace.fs.createDirectory(this.container.context.globalStorageUri); + try { + await workspace.fs.createDirectory(mcpFolderPath); + } catch (ex) { + throw new Error(`Failed to create global storage directory for MCP: ${ex}`); + } // Write the installer to the extension storage - await workspace.fs.writeFile(mcpInstallerPath, new Uint8Array(installerData)); - Logger.log(`Downloaded MCP proxy installer successfully`); + try { + await workspace.fs.writeFile(mcpInstallerPath, new Uint8Array(installerData)); + } catch (ex) { + throw new Error(`Failed to download MCP installer: ${ex}`); + } try { // Use the run function to extract the installer file from the installer zip if (platform === 'windows') { - // On Windows, use PowerShell to extract the zip file + // On Windows, use PowerShell to extract the zip file. + // Force overwrite if the file already exists and the force param is true await run( 'powershell.exe', [ '-Command', - `Expand-Archive -Path "${mcpInstallerPath.fsPath}" -DestinationPath "${this.container.context.globalStorageUri.fsPath}"`, + `Expand-Archive -Path "${mcpInstallerPath.fsPath}" -DestinationPath "${mcpFolderPath.fsPath}" -Force`, ], 'utf8', ); } else { // On Unix-like systems, use the unzip command to extract the zip file - await run( - 'unzip', - ['-o', mcpInstallerPath.fsPath, '-d', this.container.context.globalStorageUri.fsPath], - 'utf8', - ); + await run('unzip', ['-o', mcpInstallerPath.fsPath, '-d', mcpFolderPath.fsPath], 'utf8'); } - // The gk file should be in a subfolder named after the installer file name - const extractedFolderName = installerFileName.replace(/\.zip$/, ''); - mcpExtractedFolderPath = Uri.joinPath( - this.container.context.globalStorageUri, - extractedFolderName, - ); - mcpExtractedPath = Uri.joinPath(mcpExtractedFolderPath, mcpFileName); // Check using stat to make sure the newly extracted file exists. - await workspace.fs.stat(mcpExtractedPath); - void this.container.storage.store('ai:mcp:installPath', mcpExtractedFolderPath.fsPath).catch(); - } catch (error) { - const errorMsg = `Failed to extract MCP installer: ${error}`; - Logger.error(errorMsg); - throw new Error(errorMsg); - } - - // Get the app name - let appName = 'vscode'; - let isInsiders = false; - switch (env.appName) { - case 'Visual Studio Code': - break; - case 'Visual Studio Code - Insiders': - isInsiders = true; - break; - case 'Cursor': - appName = 'cursor'; - break; - case 'Windsurf': - appName = 'windsurf'; - break; - default: { - const errorMsg = `Failed to install MCP: unsupported app name - ${env.appName}`; - Logger.error(errorMsg); - throw new Error(errorMsg); - } + mcpExtractedFilePath = Uri.joinPath(mcpFolderPath, platform === 'windows' ? 'gk.exe' : 'gk'); + await workspace.fs.stat(mcpExtractedFilePath); + void this.container.storage.store('ai:mcp:installPath', mcpFolderPath.fsPath).catch(); + } catch (ex) { + throw new Error(`Failed to extract MCP installer: ${ex}`); } - // Configure the MCP server in settings.json + // Set up the local MCP server files try { - const installOutput = await run( - platform === 'windows' ? mcpFileName : `./${mcpFileName}`, - ['install'], - 'utf8', - { cwd: mcpExtractedFolderPath.fsPath }, - ); + const installOutput = await this.runMcpCommand(['install'], { + cwd: mcpFolderPath.fsPath, + }); const directory = installOutput.match(/Directory: (.*)/); let directoryPath; if (directory != null && directory.length > 1) { directoryPath = directory[1]; void this.container.storage.store('gk:cli:installedPath', directoryPath).catch(); } else { - Logger.warn('MCP Install: Failed to find directory in install output'); - if (appName === 'vscode') { - throw new Error('MCP command path not availavle'); - } + throw new Error('Failed to find directory in CLI install output'); } - if (appName === 'vscode') { - const config = { - name: 'GitKraken', - command: mcpExtractedPath.fsPath, - args: ['mcp'], - type: 'stdio', - }; - const installDeepLinkUrl = `${isInsiders ? 'vscode-insiders' : 'vscode'}:mcp/install?${encodeURIComponent(JSON.stringify(config))}`; - await openUrl(installDeepLinkUrl); - } else { - await run( - platform === 'windows' ? mcpFileName : `./${mcpFileName}`, - ['mcp', 'install', appName], - 'utf8', - { cwd: mcpExtractedFolderPath.fsPath }, - ); - } - - const gkAuth = (await this.container.subscription.getAuthenticationSession())?.accessToken; - if (gkAuth != null) { - await run( - platform === 'windows' ? mcpFileName : `./${mcpFileName}`, - ['auth', 'login', '-t', gkAuth], - 'utf8', - { cwd: mcpExtractedFolderPath.fsPath }, - ); - } - - Logger.log( - `MCP configuration completed.${appName === 'vscode' ? " Click 'install' to install." : ''}`, - ); - } catch (error) { - const errorMsg = `MCP server configuration failed: ${error}`; - Logger.error(errorMsg); - throw new Error(errorMsg); + Logger.log('MCP setup completed.'); + void this.container.storage.store('ai:mcp:install', 'completed').catch(); + await this.authMCPServer(); + } catch (ex) { + throw new Error(`MCP server configuration failed: ${ex}`); } } finally { + // Clean up the installer zip file if (mcpInstallerPath != null) { try { await workspace.fs.delete(mcpInstallerPath); - } catch (error) { - Logger.warn(`Failed to delete MCP installer zip file: ${error}`); + } catch (ex) { + Logger.warn(`Failed to delete MCP installer zip file: ${ex}`); } } + + try { + const readmePath = Uri.joinPath(mcpFolderPath, 'README.md'); + await workspace.fs.delete(readmePath); + } catch {} } }; // Execute the installation task with or without progress indicator - if (silent !== true) { + if (!autoInstall) { await window.withProgress( { location: ProgressLocation.Notification, - title: 'Installing MCP integration...', + title: 'Setting up MCP integration...', cancellable: false, }, async () => { @@ -370,48 +372,57 @@ export class GkCliIntegrationProvider implements Disposable { } else { await installationTask(); } - - // Show success notification if not silent - void this.container.storage.store('ai:mcp:attemptInstall', 'completed').catch(); - void window.showInformationMessage('GitKraken MCP integration installed successfully'); - } catch (error) { - Logger.error(`Error during MCP installation: ${error}`); - - // Show error notification if not silent - if (silent !== true) { - void window.showErrorMessage( - `Failed to install MCP integration: ${error instanceof Error ? error.message : String(error)}`, - ); + } catch (ex) { + const errorMsg = `Failed to configure MCP: ${ex instanceof Error ? ex.message : String(ex)}`; + if (!autoInstall) { + throw new Error(errorMsg); + } else { + Logger.error(errorMsg); } } } - private async onSubscriptionChanged(e: SubscriptionChangeEvent): Promise { - const mcpInstallStatus = this.container.storage.get('ai:mcp:attemptInstall'); + private async runMcpCommand( + args: string[], + options?: { + cwd?: string; + }, + ): Promise { + const platform = getPlatform(); + const cwd = options?.cwd ?? this.container.storage.get('ai:mcp:installPath'); + if (cwd == null) { + throw new Error('MCP is not installed'); + } + + return run(platform === 'windows' ? 'gk.exe' : './gk', args, 'utf8', { cwd: cwd }); + } + + private async authMCPServer(): Promise { + const mcpInstallStatus = this.container.storage.get('ai:mcp:install'); const mcpInstallPath = this.container.storage.get('ai:mcp:installPath'); + if (mcpInstallStatus !== 'completed' || mcpInstallPath == null) { + return; + } - const platform = getPlatform(); - if ( - e.current?.account?.id != null && - e.current.account.id !== e.previous?.account?.id && - mcpInstallStatus === 'completed' && - mcpInstallPath != null - ) { - const currentSessionToken = (await this.container.subscription.getAuthenticationSession())?.accessToken; - if (currentSessionToken != null) { - try { - await run( - platform === 'windows' ? 'gk.exe' : './gk', - ['auth', 'login', '-t', currentSessionToken], - 'utf8', - { cwd: mcpInstallPath }, - ); - } catch {} - } + const currentSessionToken = (await this.container.subscription.getAuthenticationSession())?.accessToken; + if (currentSessionToken == null) { + return; + } + + try { + await this.runMcpCommand(['auth', 'login', '-t', currentSessionToken]); + } catch (ex) { + Logger.error(`Failed to auth MCP server: ${ex instanceof Error ? ex.message : String(ex)}`); + } + } + + private async onSubscriptionChanged(e: SubscriptionChangeEvent): Promise { + if (e.current?.account?.id != null && e.current.account.id !== e.previous?.account?.id) { + await this.authMCPServer(); } } private registerCommands(): Disposable[] { - return [registerCommand('gitlens.ai.mcp.install', () => this.installMCPIfNeeded())]; + return [registerCommand('gitlens.ai.mcp.install', () => this.installMCP())]; } } From 98666733b90b9d1869b2daa86b4a10005204eb0b Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Mon, 25 Aug 2025 10:36:45 -0700 Subject: [PATCH 03/33] Adds gates and deeplink command support --- docs/links.md | 2 ++ src/env/node/gk/cli/integration.ts | 3 +++ src/uris/deepLinks/deepLink.ts | 2 ++ 3 files changed, 7 insertions(+) diff --git a/docs/links.md b/docs/links.md index 916b3b847e17f..177512ecfe17f 100644 --- a/docs/links.md +++ b/docs/links.md @@ -274,6 +274,8 @@ _{prefix}/command/{command}_ - _inspect_ - Runs the `GitLens: Inspect Commit Details` command. + - _install-mcp_ - Runs the `GitLens: Install MCP` command. + - _login_ - Runs the `GitLens: Sign In to GitKraken...` command. - _signup_ - Runs the `GitLens: Sign Up for GitKraken...` command. diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index 7a0e3589db6ef..e2250519664f5 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -7,6 +7,7 @@ import { registerCommand } from '../../../../system/-webview/command'; import { configuration } from '../../../../system/-webview/configuration'; import { getContext } from '../../../../system/-webview/context'; import { openUrl } from '../../../../system/-webview/vscode/uris'; +import { gate } from '../../../../system/decorators/gate'; import { Logger } from '../../../../system/logger'; import { getLogScope } from '../../../../system/logger.scope'; import { compare } from '../../../../system/version'; @@ -76,6 +77,7 @@ export class GkCliIntegrationProvider implements Disposable { this._runningDisposable = undefined; } + @gate() private async installMCP(): Promise { const scope = getLogScope(); try { @@ -168,6 +170,7 @@ export class GkCliIntegrationProvider implements Disposable { } } + @gate() private async setupMCPInstallation(autoInstall?: boolean): Promise { try { if ( diff --git a/src/uris/deepLinks/deepLink.ts b/src/uris/deepLinks/deepLink.ts index 4cb33d3cc67b2..9eab3248561bb 100644 --- a/src/uris/deepLinks/deepLink.ts +++ b/src/uris/deepLinks/deepLink.ts @@ -25,6 +25,7 @@ export enum DeepLinkCommandType { Graph = 'graph', Home = 'home', Inspect = 'inspect', + InstallMCP = 'install-mcp', Launchpad = 'launchpad', Login = 'login', SignUp = 'signup', @@ -46,6 +47,7 @@ export const DeepLinkCommandTypeToCommand = new Map Date: Tue, 26 Aug 2025 10:42:54 -0700 Subject: [PATCH 04/33] Updates to include new params --- src/env/node/gk/cli/integration.ts | 56 +++++++++++++++--------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index e2250519664f5..16596d6a28bed 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -90,12 +90,11 @@ export class GkCliIntegrationProvider implements Disposable { } let appName = 'vscode'; - let isInsiders = false; switch (env.appName) { case 'Visual Studio Code': break; case 'Visual Studio Code - Insiders': - isInsiders = true; + appName = 'vscode-insiders'; break; case 'Cursor': appName = 'cursor'; @@ -132,35 +131,36 @@ export class GkCliIntegrationProvider implements Disposable { return; } - // TODO: REMOVE THIS ONCE VSCODE-INSIDERS IS ADDED AS AN OFFICIAL PROVIDER TO MCP INSTALL COMMAND - if (appName === 'vscode' && isInsiders) { - const mcpFileName = getPlatform() === 'windows' ? 'gk.exe' : 'gk'; - const mcpProxyPath = Uri.joinPath(Uri.file(mcpPath), mcpFileName); - const config = { - name: 'GitKraken', - command: mcpProxyPath.fsPath, - args: ['mcp'], - type: 'stdio', - }; - const installDeepLinkUrl = `vscode-insiders:mcp/install?${encodeURIComponent(JSON.stringify(config))}`; - await openUrl(installDeepLinkUrl); - } else { - if (appName !== 'cursor' && appName !== 'vscode') { - const confirmation = await window.showInformationMessage( - `MCP configured successfully. Click 'Finish' to add it to your MCP server list and complete the installation.`, - { modal: true }, - { title: 'Finish' }, - { title: 'Cancel', isCloseAffordance: true }, - ); - if (confirmation == null || confirmation.title === 'Cancel') return; - } + if (appName !== 'cursor' && appName !== 'vscode') { + const confirmation = await window.showInformationMessage( + `MCP configured successfully. Click 'Finish' to add it to your MCP server list and complete the installation.`, + { modal: true }, + { title: 'Finish' }, + { title: 'Cancel', isCloseAffordance: true }, + ); + if (confirmation == null || confirmation.title === 'Cancel') return; + } - const _output = await this.runMcpCommand(['mcp', 'install', appName, '--source=gitlens'], { + let output = await this.runMcpCommand( + ['mcp', 'install', appName, '--source=gitlens', `--scheme=${env.uriScheme}`], + { cwd: mcpPath, - }); - // TODO: GET THE INSTALL LINK FROM THE OUTPUT IF IT EXISTS AND OPEN IT. - // CURRENTLY THE CLI TRIES TO DO SO BUT THE LINK DOES NOT WORK SINCE IT IS IN THE CHILD PROCESS. + }, + ); + + output = output.trim(); + if (output === 'GitKraken MCP Server Successfully Installed!') { + return; } + + // Check if the output is a valid url. If so, run it + try { + new URL(output); + } catch { + throw new Error('Failed to install MCP integration: unexpected output from mcp install command'); + } + + await openUrl(output); } catch (ex) { Logger.error(`Error during MCP installation: ${ex}`, scope); From 8a9c4a741f4c372cfa010592752de48bb0191019 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Tue, 26 Aug 2025 15:09:35 -0700 Subject: [PATCH 05/33] Updates terminology and adds telemetry --- contributions.json | 2 +- docs/telemetry-events.md | 63 ++++ package.json | 2 +- src/constants.storage.ts | 6 +- src/constants.telemetry.ts | 39 ++ src/env/node/gk/cli/integration.ts | 573 ++++++++++++++++++++--------- 6 files changed, 498 insertions(+), 187 deletions(-) diff --git a/contributions.json b/contributions.json index 2a2402f598c25..29846db381f11 100644 --- a/contributions.json +++ b/contributions.json @@ -312,7 +312,7 @@ "commandPalette": "gitlens:enabled && !gitlens:untrusted && gitlens:gk:organization:ai:enabled" }, "gitlens.ai.mcp.install": { - "label": "Install MCP Server", + "label": "Install GitKraken MCP Server", "commandPalette": "gitlens:enabled && !gitlens:untrusted && gitlens:gk:organization:ai:enabled" }, "gitlens.ai.rebaseOntoCommit:graph": { diff --git a/docs/telemetry-events.md b/docs/telemetry-events.md index b6cf313c9f361..91cf23be38c34 100644 --- a/docs/telemetry-events.md +++ b/docs/telemetry-events.md @@ -570,6 +570,40 @@ void } ``` +### cli/install/failed + +> Sent when a CLI install attempt fails + +```typescript +{ + 'attempts': number, + 'autoInstall': boolean, + 'error.message': string +} +``` + +### cli/install/started + +> Sent when a CLI install attempt is started + +```typescript +{ + 'attempts': number, + 'autoInstall': boolean +} +``` + +### cli/install/succeeded + +> Sent when a CLI install attempt succeeds + +```typescript +{ + 'attempts': number, + 'autoInstall': boolean +} +``` + ### cloudIntegrations/connected > Sent when connected to one or more cloud-based integrations from gkdev @@ -2726,6 +2760,35 @@ void } ``` +### mcp/setup/completed + +> Sent when GitKraken MCP setup is completed + +```typescript +{ + 'requiresUserCompletion': boolean +} +``` + +### mcp/setup/failed + +> Sent when GitKraken MCP setup fails + +```typescript +{ + 'error.message': string, + 'reason': string +} +``` + +### mcp/setup/started + +> Sent when GitKraken MCP setup is started + +```typescript +void +``` + ### openReviewMode > Sent when a PR review was started in the inspect overview diff --git a/package.json b/package.json index 78e792c54391c..99085f90823d7 100644 --- a/package.json +++ b/package.json @@ -6298,7 +6298,7 @@ }, { "command": "gitlens.ai.mcp.install", - "title": "Install MCP Server", + "title": "Install GitKraken MCP Server", "category": "GitLens" }, { diff --git a/src/constants.storage.ts b/src/constants.storage.ts index 634332eecde74..ae6f0047c7c34 100644 --- a/src/constants.storage.ts +++ b/src/constants.storage.ts @@ -63,8 +63,6 @@ export type DeprecatedGlobalStorage = { }; export type GlobalStorage = { - 'ai:mcp:install': string; - 'ai:mcp:installPath': string; avatars: [string, StoredAvatar][]; 'confirm:ai:generateCommits': boolean; 'confirm:ai:generateRebase': boolean; @@ -85,7 +83,9 @@ export type GlobalStorage = { // Value based on `currentOnboardingVersion` in composer's protocol 'composer:onboarding:dismissed': string; 'composer:onboarding:stepReached': number; - 'gk:cli:installedPath': string; + 'gk:cli:install': { status: 'attempted' | 'unsupported' | 'completed'; attempts: number }; + 'gk:cli:corePath': string; + 'gk:cli:path': string; 'home:sections:collapsed': string[]; 'home:walkthrough:dismissed': boolean; 'launchpad:groups:collapsed': StoredLaunchpadGroup[]; diff --git a/src/constants.telemetry.ts b/src/constants.telemetry.ts index a61ab58c9c105..defbe0b0a5877 100644 --- a/src/constants.telemetry.ts +++ b/src/constants.telemetry.ts @@ -79,6 +79,13 @@ export interface TelemetryEvents extends WebviewShowAbortedEvents, WebviewShownE /** Sent when user opts in to AI All Access */ 'aiAllAccess/optedIn': void; + /** Sent when a CLI install attempt is started */ + 'cli/install/started': CLIInstallStartedEvent; + /** Sent when a CLI install attempt succeeds */ + 'cli/install/succeeded': CLIInstallSucceededEvent; + /** Sent when a CLI install attempt fails */ + 'cli/install/failed': CLIInstallFailedEvent; + /** Sent when connecting to one or more cloud-based integrations */ 'cloudIntegrations/connecting': CloudIntegrationsConnectingEvent; @@ -238,6 +245,13 @@ export interface TelemetryEvents extends WebviewShowAbortedEvents, WebviewShownE /** Sent when a launchpad operation is taking longer than a set timeout to complete */ 'launchpad/operation/slow': LaunchpadOperationSlowEvent; + /** Sent when GitKraken MCP setup is started */ + 'mcp/setup/started': void; + /** Sent when GitKraken MCP setup is completed */ + 'mcp/setup/completed': MCPSetupCompletedEvent; + /** Sent when GitKraken MCP setup fails */ + 'mcp/setup/failed': MCPSetupFailedEvent; + /** Sent when a PR review was started in the inspect overview */ openReviewMode: OpenReviewModeEvent; @@ -472,6 +486,31 @@ export interface AIFeedbackEvent extends AIEventDataBase { 'unhelpful.custom'?: string; } +export interface CLIInstallStartedEvent { + autoInstall: boolean; + attempts: number; +} + +export interface CLIInstallSucceededEvent { + autoInstall: boolean; + attempts: number; +} + +export interface CLIInstallFailedEvent { + autoInstall: boolean; + attempts: number; + 'error.message'?: string; +} + +export interface MCPSetupCompletedEvent { + requiresUserCompletion: boolean; +} + +export interface MCPSetupFailedEvent { + reason: string; + 'error.message'?: string; +} + interface CloudIntegrationsConnectingEvent { 'integration.ids': string | undefined; } diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index 16596d6a28bed..cf1c3113f146c 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -5,7 +5,6 @@ import type { Container } from '../../../../container'; import type { SubscriptionChangeEvent } from '../../../../plus/gk/subscriptionService'; import { registerCommand } from '../../../../system/-webview/command'; import { configuration } from '../../../../system/-webview/configuration'; -import { getContext } from '../../../../system/-webview/context'; import { openUrl } from '../../../../system/-webview/vscode/uris'; import { gate } from '../../../../system/decorators/gate'; import { Logger } from '../../../../system/logger'; @@ -17,6 +16,18 @@ import { CliCommandHandlers } from './commands'; import type { IpcServer } from './ipcServer'; import { createIpcServer } from './ipcServer'; +const enum CLIInstallErrorReason { + WebEnvironmentUnsupported, + UnsupportedPlatform, + ProxyUrlFetch, + ProxyUrlFormat, + ProxyDownload, + ProxyExtract, + ProxyFetch, + CoreDirectory, + CoreInstall, +} + export interface CliCommandRequest { cwd?: string; args?: string[]; @@ -37,9 +48,9 @@ export class GkCliIntegrationProvider implements Disposable { this.onConfigurationChanged(); - const mcpInstallStatus = this.container.storage.get('ai:mcp:install'); - if (!mcpInstallStatus) { - setTimeout(() => this.setupMCPInstallation(true), 10000 + Math.floor(Math.random() * 20000)); + const cliInstall = this.container.storage.get('gk:cli:install'); + if (!cliInstall || (cliInstall.status === 'attempted' && cliInstall.attempts < 5)) { + setTimeout(() => this.installCLI(true), 10000 + Math.floor(Math.random() * 20000)); } } @@ -78,14 +89,23 @@ export class GkCliIntegrationProvider implements Disposable { } @gate() - private async installMCP(): Promise { + private async setupMCP(): Promise { const scope = getLogScope(); + if (this.container.telemetry.enabled) { + this.container.telemetry.sendEvent('mcp/setup/started'); + } + try { if ( (env.appName === 'Visual Studio Code' || env.appName === 'Visual Studio Code - Insiders') && compare(codeVersion, '1.102') < 0 ) { void window.showInformationMessage('Use of this command requires VS Code 1.102 or later.'); + if (this.container.telemetry.enabled) { + this.container.telemetry.sendEvent('mcp/setup/failed', { + reason: 'unsupported vscode version', + }); + } return; } @@ -104,47 +124,114 @@ export class GkCliIntegrationProvider implements Disposable { break; default: { void window.showInformationMessage(`MCP installation is not supported for app: ${env.appName}`); + if (this.container.telemetry.enabled) { + this.container.telemetry.sendEvent('mcp/setup/failed', { + reason: 'unsupported app', + }); + } return; } } - let autoInstallProgress = this.container.storage.get('ai:mcp:install'); - let mcpPath = this.container.storage.get('ai:mcp:installPath'); - let mcpFileExists = true; - if (mcpPath != null) { + let cliInstall = this.container.storage.get('gk:cli:install'); + let cliPath = this.container.storage.get('gk:cli:path'); + let cliProxyFileExists = true; + if (cliPath != null) { try { await workspace.fs.stat( - Uri.joinPath(Uri.file(mcpPath), getPlatform() === 'windows' ? 'gk.exe' : 'gk'), + Uri.joinPath(Uri.file(cliPath), getPlatform() === 'windows' ? 'gk.exe' : 'gk'), ); } catch { - mcpFileExists = false; + cliProxyFileExists = false; } } - if (autoInstallProgress !== 'completed' || mcpPath == null || !mcpFileExists) { - await this.setupMCPInstallation(); + if (cliInstall?.status !== 'completed' || cliPath == null || !cliProxyFileExists) { + try { + await window.withProgress( + { + location: ProgressLocation.Notification, + title: 'Setting up MCP integration...', + cancellable: false, + }, + async () => { + await this.installCLI(); + }, + ); + } catch (ex) { + let failureReason = 'unknown error'; + if (ex instanceof CLIInstallError) { + switch (ex.reason) { + case CLIInstallErrorReason.WebEnvironmentUnsupported: + void window.showErrorMessage( + 'MCP installation is not supported in the web environment.', + ); + failureReason = 'web environment unsupported'; + break; + case CLIInstallErrorReason.UnsupportedPlatform: + void window.showErrorMessage('MCP installation is not supported on this platform.'); + failureReason = 'unsupported platform'; + break; + case CLIInstallErrorReason.ProxyUrlFetch: + case CLIInstallErrorReason.ProxyUrlFormat: + case CLIInstallErrorReason.ProxyFetch: + case CLIInstallErrorReason.ProxyDownload: + case CLIInstallErrorReason.ProxyExtract: + case CLIInstallErrorReason.CoreDirectory: + case CLIInstallErrorReason.CoreInstall: + void window.showErrorMessage('Failed to install MCP server locally.'); + failureReason = 'local installation failed'; + break; + default: + void window.showErrorMessage( + `Failed to install MCP integration: ${ex instanceof Error ? ex.message : 'Unknown error during installation'}`, + ); + break; + } + } + + if (this.container.telemetry.enabled) { + this.container.telemetry.sendEvent('mcp/setup/failed', { + reason: failureReason, + 'error.message': ex instanceof Error ? ex.message : 'Unknown error during installation', + }); + } + } } - autoInstallProgress = this.container.storage.get('ai:mcp:install'); - mcpPath = this.container.storage.get('ai:mcp:installPath'); - if (autoInstallProgress !== 'completed' || mcpPath == null) { - void window.showErrorMessage('Failed to install MCP integration: setup failed to complete.'); + cliInstall = this.container.storage.get('gk:cli:install'); + cliPath = this.container.storage.get('gk:cli:path'); + if (cliInstall?.status !== 'completed' || cliPath == null) { + void window.showErrorMessage('Failed to install MCP integration: Unknown error during installation.'); + if (this.container.telemetry.enabled) { + this.container.telemetry.sendEvent('mcp/setup/failed', { + reason: 'unknown error', + 'error.message': 'Unknown error during installation', + }); + } return; } - if (appName !== 'cursor' && appName !== 'vscode') { + if (appName !== 'cursor' && appName !== 'vscode' && appName !== 'vscode-insiders') { const confirmation = await window.showInformationMessage( `MCP configured successfully. Click 'Finish' to add it to your MCP server list and complete the installation.`, { modal: true }, { title: 'Finish' }, { title: 'Cancel', isCloseAffordance: true }, ); - if (confirmation == null || confirmation.title === 'Cancel') return; + if (confirmation == null || confirmation.title === 'Cancel') { + if (this.container.telemetry.enabled) { + this.container.telemetry.sendEvent('mcp/setup/failed', { + reason: 'user cancelled', + }); + } + return; + } } - let output = await this.runMcpCommand( + let output = await this.runCLICommand( ['mcp', 'install', appName, '--source=gitlens', `--scheme=${env.uriScheme}`], { - cwd: mcpPath, + cwd: cliPath, }, ); @@ -157,12 +244,32 @@ export class GkCliIntegrationProvider implements Disposable { try { new URL(output); } catch { - throw new Error('Failed to install MCP integration: unexpected output from mcp install command'); + if (this.container.telemetry.enabled) { + this.container.telemetry.sendEvent('mcp/setup/failed', { + reason: 'unexpected output from mcp install command', + 'error.message': `Unexpected output from mcp install command: ${output}`, + }); + } + Logger.error(`Unexpected output from mcp install command: ${output}`, scope); + void window.showErrorMessage(`Failed to install MCP integration: error getting install URL`); + return; } await openUrl(output); + if (this.container.telemetry.enabled) { + this.container.telemetry.sendEvent('mcp/setup/completed', { + requiresUserCompletion: + appName === 'cursor' || appName === 'vscode' || appName === 'vscode-insiders', + }); + } } catch (ex) { Logger.error(`Error during MCP installation: ${ex}`, scope); + if (this.container.telemetry.enabled) { + this.container.telemetry.sendEvent('mcp/setup/failed', { + reason: 'unknown error', + 'error.message': ex instanceof Error ? ex.message : 'Unknown error during installation', + }); + } void window.showErrorMessage( `Failed to install MCP integration: ${ex instanceof Error ? ex.message : String(ex)}`, @@ -171,32 +278,34 @@ export class GkCliIntegrationProvider implements Disposable { } @gate() - private async setupMCPInstallation(autoInstall?: boolean): Promise { + private async installCLI(autoInstall?: boolean): Promise { + let attempts = 0; try { - if ( - (env.appName === 'Visual Studio Code' || env.appName === 'Visual Studio Code - Insiders') && - compare(codeVersion, '1.102') < 0 - ) { - return; - } - - // Kick out early if we already attempted an auto-install - if (autoInstall && this.container.storage.get('ai:mcp:install')) { - return; - } - - void this.container.storage.store('ai:mcp:install', 'attempted').catch(); - - if (configuration.get('ai.enabled') === false) { - throw new Error('AI is disabled in settings'); - } - - if (getContext('gitlens:gk:organization:ai:enabled', true) !== true) { - throw new Error('AI is disabled by your organization'); + const cliInstall = this.container.storage.get('gk:cli:install'); + attempts = cliInstall?.attempts ?? 0; + attempts += 1; + if (this.container.telemetry.enabled) { + this.container.telemetry.sendEvent('cli/install/started', { + autoInstall: autoInstall ?? false, + attempts: attempts, + }); } + void this.container.storage + .store('gk:cli:install', { + status: 'attempted', + attempts: attempts, + }) + .catch(); if (isWeb) { - throw new Error('Web environment is not supported'); + void this.container.storage + .store('gk:cli:install', { + status: 'unsupported', + attempts: attempts, + }) + .catch(); + + throw new CLIInstallError(CLIInstallErrorReason.WebEnvironmentUnsupported); } // Detect platform and architecture @@ -229,181 +338,222 @@ export class GkCliIntegrationProvider implements Disposable { platformName = 'linux'; break; default: { - throw new Error(`Unsupported platform ${platform}`); + void this.container.storage + .store('gk:cli:install', { + status: 'unsupported', + attempts: attempts, + }) + .catch(); + throw new CLIInstallError(CLIInstallErrorReason.UnsupportedPlatform, undefined, platform); } } - // Wrap the main installation process with progress indicator if not silent - const installationTask = async () => { - let mcpInstallerPath: Uri | undefined; - let mcpExtractedFilePath: Uri | undefined; - const mcpFolderPath = this.container.context.globalStorageUri; + let cliProxyZipFilePath: Uri | undefined; + let cliExtractedProxyFilePath: Uri | undefined; + const globalStoragePath = this.container.context.globalStorageUri; - try { - // Download the MCP proxy installer - const proxyUrl = this.container.urls.getGkApiUrl( - 'releases', - 'gkcli-proxy', - 'production', - platformName, - architecture, - 'active', + try { + // Download the MCP proxy installer + const proxyUrl = this.container.urls.getGkApiUrl( + 'releases', + 'gkcli-proxy', + 'production', + platformName, + architecture, + 'active', + ); + + let response = await fetch(proxyUrl); + if (!response.ok) { + throw new CLIInstallError( + CLIInstallErrorReason.ProxyUrlFetch, + undefined, + `${response.status} ${response.statusText}`, ); + } - let response = await fetch(proxyUrl); - if (!response.ok) { - throw new Error(`Failed to get MCP installer info: ${response.status} ${response.statusText}`); - } + let downloadUrl: string | undefined; + try { + const cliZipArchiveDownloadInfo: { version?: string; packages?: { zip?: string } } | undefined = + (await response.json()) as any; + downloadUrl = cliZipArchiveDownloadInfo?.packages?.zip; + } catch (ex) { + throw new CLIInstallError( + CLIInstallErrorReason.ProxyUrlFormat, + ex instanceof Error ? ex : undefined, + ex instanceof Error ? ex.message : undefined, + ); + } - let downloadUrl: string | undefined; - try { - const mcpInstallerInfo: { version?: string; packages?: { zip?: string } } | undefined = - (await response.json()) as any; - downloadUrl = mcpInstallerInfo?.packages?.zip; - } catch (ex) { - throw new Error(`Failed to parse MCP installer info: ${ex}`); - } + if (downloadUrl == null) { + throw new CLIInstallError( + CLIInstallErrorReason.ProxyUrlFormat, + undefined, + 'No download URL found for CLI proxy archive', + ); + } - if (downloadUrl == null) { - throw new Error('Failed to find download URL for MCP proxy installer'); - } + response = await fetch(downloadUrl); + if (!response.ok) { + throw new CLIInstallError( + CLIInstallErrorReason.ProxyFetch, + undefined, + `${response.status} ${response.statusText}`, + ); + } - response = await fetch(downloadUrl); - if (!response.ok) { - throw new Error( - `Failed to fetch MCP proxy installer: ${response.status} ${response.statusText}`, - ); - } + const cliProxyZipFileDownloadData = await response.arrayBuffer(); + if (cliProxyZipFileDownloadData.byteLength === 0) { + throw new CLIInstallError( + CLIInstallErrorReason.ProxyDownload, + undefined, + 'Downloaded proxy archive data is empty', + ); + } + // installer file name is the last part of the download URL + const cliProxyZipFileName = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1); + cliProxyZipFilePath = Uri.joinPath(globalStoragePath, cliProxyZipFileName); - const installerData = await response.arrayBuffer(); - if (installerData.byteLength === 0) { - throw new Error('Fetched MCP installer is empty'); - } - // installer file name is the last part of the download URL - const installerFileName = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1); - mcpInstallerPath = Uri.joinPath(mcpFolderPath, installerFileName); + // Ensure the global storage directory exists + try { + await workspace.fs.createDirectory(globalStoragePath); + } catch (ex) { + throw new CLIInstallError( + CLIInstallErrorReason.CoreDirectory, + ex instanceof Error ? ex : undefined, + ex instanceof Error ? ex.message : undefined, + ); + } - // Ensure the global storage directory exists - try { - await workspace.fs.createDirectory(mcpFolderPath); - } catch (ex) { - throw new Error(`Failed to create global storage directory for MCP: ${ex}`); - } + // Write the installer to the extension storage + try { + await workspace.fs.writeFile(cliProxyZipFilePath, new Uint8Array(cliProxyZipFileDownloadData)); + } catch (ex) { + throw new CLIInstallError( + CLIInstallErrorReason.ProxyDownload, + ex instanceof Error ? ex : undefined, + 'Failed to write proxy archive to global storage', + ); + } - // Write the installer to the extension storage - try { - await workspace.fs.writeFile(mcpInstallerPath, new Uint8Array(installerData)); - } catch (ex) { - throw new Error(`Failed to download MCP installer: ${ex}`); + try { + // Use the run function to extract the installer file from the installer zip + if (platform === 'windows') { + // On Windows, use PowerShell to extract the zip file. + // Force overwrite if the file already exists and the force param is true + await run( + 'powershell.exe', + [ + '-Command', + `Expand-Archive -Path "${cliProxyZipFilePath.fsPath}" -DestinationPath "${globalStoragePath.fsPath}" -Force`, + ], + 'utf8', + ); + } else { + // On Unix-like systems, use the unzip command to extract the zip file + await run('unzip', ['-o', cliProxyZipFilePath.fsPath, '-d', globalStoragePath.fsPath], 'utf8'); } - try { - // Use the run function to extract the installer file from the installer zip - if (platform === 'windows') { - // On Windows, use PowerShell to extract the zip file. - // Force overwrite if the file already exists and the force param is true - await run( - 'powershell.exe', - [ - '-Command', - `Expand-Archive -Path "${mcpInstallerPath.fsPath}" -DestinationPath "${mcpFolderPath.fsPath}" -Force`, - ], - 'utf8', - ); - } else { - // On Unix-like systems, use the unzip command to extract the zip file - await run('unzip', ['-o', mcpInstallerPath.fsPath, '-d', mcpFolderPath.fsPath], 'utf8'); - } + // Check using stat to make sure the newly extracted file exists. + cliExtractedProxyFilePath = Uri.joinPath( + globalStoragePath, + platform === 'windows' ? 'gk.exe' : 'gk', + ); + await workspace.fs.stat(cliExtractedProxyFilePath); + void this.container.storage.store('gk:cli:path', globalStoragePath.fsPath).catch(); + } catch (ex) { + throw new CLIInstallError( + CLIInstallErrorReason.ProxyExtract, + ex instanceof Error ? ex : undefined, + ex instanceof Error ? ex.message : '', + ); + } - // Check using stat to make sure the newly extracted file exists. - mcpExtractedFilePath = Uri.joinPath(mcpFolderPath, platform === 'windows' ? 'gk.exe' : 'gk'); - await workspace.fs.stat(mcpExtractedFilePath); - void this.container.storage.store('ai:mcp:installPath', mcpFolderPath.fsPath).catch(); - } catch (ex) { - throw new Error(`Failed to extract MCP installer: ${ex}`); + // Set up the local MCP server files + try { + const coreInstallOutput = await this.runCLICommand(['install'], { + cwd: globalStoragePath.fsPath, + }); + const directory = coreInstallOutput.match(/Directory: (.*)/); + let directoryPath; + if (directory != null && directory.length > 1) { + directoryPath = directory[1]; + void this.container.storage.store('gk:cli:corePath', directoryPath).catch(); + } else { + throw new CLIInstallError(CLIInstallErrorReason.CoreDirectory); } - // Set up the local MCP server files - try { - const installOutput = await this.runMcpCommand(['install'], { - cwd: mcpFolderPath.fsPath, + Logger.log('CLI install completed.'); + void this.container.storage + .store('gk:cli:install', { status: 'completed', attempts: attempts }) + .catch(); + if (this.container.telemetry.enabled) { + this.container.telemetry.sendEvent('cli/install/succeeded', { + autoInstall: autoInstall ?? false, + attempts: attempts, }); - const directory = installOutput.match(/Directory: (.*)/); - let directoryPath; - if (directory != null && directory.length > 1) { - directoryPath = directory[1]; - void this.container.storage.store('gk:cli:installedPath', directoryPath).catch(); - } else { - throw new Error('Failed to find directory in CLI install output'); - } - - Logger.log('MCP setup completed.'); - void this.container.storage.store('ai:mcp:install', 'completed').catch(); - await this.authMCPServer(); - } catch (ex) { - throw new Error(`MCP server configuration failed: ${ex}`); } - } finally { - // Clean up the installer zip file - if (mcpInstallerPath != null) { - try { - await workspace.fs.delete(mcpInstallerPath); - } catch (ex) { - Logger.warn(`Failed to delete MCP installer zip file: ${ex}`); - } - } - + await this.authCLI(); + } catch (ex) { + throw new CLIInstallError( + CLIInstallErrorReason.CoreInstall, + ex instanceof Error ? ex : undefined, + ex instanceof Error ? ex.message : '', + ); + } + } finally { + // Clean up the installer zip file + if (cliProxyZipFilePath != null) { try { - const readmePath = Uri.joinPath(mcpFolderPath, 'README.md'); - await workspace.fs.delete(readmePath); - } catch {} + await workspace.fs.delete(cliProxyZipFilePath); + } catch (ex) { + Logger.warn(`Failed to delete CLI proxy archive: ${ex}`); + } } - }; - // Execute the installation task with or without progress indicator - if (!autoInstall) { - await window.withProgress( - { - location: ProgressLocation.Notification, - title: 'Setting up MCP integration...', - cancellable: false, - }, - async () => { - await installationTask(); - }, - ); - } else { - await installationTask(); + try { + const readmePath = Uri.joinPath(globalStoragePath, 'README.md'); + await workspace.fs.delete(readmePath); + } catch (ex) { + Logger.warn(`Failed to delete CLI proxy README: ${ex}`); + } } } catch (ex) { - const errorMsg = `Failed to configure MCP: ${ex instanceof Error ? ex.message : String(ex)}`; + Logger.error( + `Failed to ${autoInstall ? 'auto-install' : 'install'} CLI: ${ex instanceof Error ? ex.message : 'Unknown error during installation'}`, + ); + if (this.container.telemetry.enabled) { + this.container.telemetry.sendEvent('cli/install/failed', { + autoInstall: autoInstall ?? false, + attempts: attempts, + 'error.message': ex instanceof Error ? ex.message : 'Unknown error', + }); + } if (!autoInstall) { - throw new Error(errorMsg); - } else { - Logger.error(errorMsg); + throw ex; } } } - private async runMcpCommand( + private async runCLICommand( args: string[], options?: { cwd?: string; }, ): Promise { const platform = getPlatform(); - const cwd = options?.cwd ?? this.container.storage.get('ai:mcp:installPath'); + const cwd = options?.cwd ?? this.container.storage.get('gk:cli:path'); if (cwd == null) { - throw new Error('MCP is not installed'); + throw new Error('CLI is not installed'); } return run(platform === 'windows' ? 'gk.exe' : './gk', args, 'utf8', { cwd: cwd }); } - private async authMCPServer(): Promise { - const mcpInstallStatus = this.container.storage.get('ai:mcp:install'); - const mcpInstallPath = this.container.storage.get('ai:mcp:installPath'); - if (mcpInstallStatus !== 'completed' || mcpInstallPath == null) { + private async authCLI(): Promise { + const cliInstall = this.container.storage.get('gk:cli:install'); + const cliPath = this.container.storage.get('gk:cli:path'); + if (cliInstall?.status !== 'completed' || cliPath == null) { return; } @@ -413,19 +563,78 @@ export class GkCliIntegrationProvider implements Disposable { } try { - await this.runMcpCommand(['auth', 'login', '-t', currentSessionToken]); + await this.runCLICommand(['auth', 'login', '-t', currentSessionToken]); } catch (ex) { - Logger.error(`Failed to auth MCP server: ${ex instanceof Error ? ex.message : String(ex)}`); + Logger.error(`Failed to auth CLI: ${ex instanceof Error ? ex.message : String(ex)}`); } } private async onSubscriptionChanged(e: SubscriptionChangeEvent): Promise { if (e.current?.account?.id != null && e.current.account.id !== e.previous?.account?.id) { - await this.authMCPServer(); + await this.authCLI(); } } private registerCommands(): Disposable[] { - return [registerCommand('gitlens.ai.mcp.install', () => this.installMCP())]; + return [registerCommand('gitlens.ai.mcp.install', () => this.setupMCP())]; + } +} + +class CLIInstallError extends Error { + readonly original?: Error; + readonly reason: CLIInstallErrorReason; + + static is(ex: unknown, reason?: CLIInstallErrorReason): ex is CLIInstallError { + return ex instanceof CLIInstallError && (reason == null || ex.reason === reason); + } + + constructor(reason: CLIInstallErrorReason, original?: Error, details?: string) { + const message = CLIInstallError.buildErrorMessage(reason, details); + super(message); + this.original = original; + this.reason = reason; + Error.captureStackTrace?.(this, CLIInstallError); + } + + private static buildErrorMessage(reason: CLIInstallErrorReason, details?: string): string { + let message; + switch (reason) { + case CLIInstallErrorReason.WebEnvironmentUnsupported: + message = 'Web environment is not supported'; + break; + case CLIInstallErrorReason.UnsupportedPlatform: + message = 'Unsupported platform'; + break; + case CLIInstallErrorReason.ProxyUrlFetch: + message = 'Failed to fetch proxy URL'; + break; + case CLIInstallErrorReason.ProxyUrlFormat: + message = 'Failed to parse proxy URL'; + break; + case CLIInstallErrorReason.ProxyDownload: + message = 'Failed to download proxy'; + break; + case CLIInstallErrorReason.ProxyExtract: + message = 'Failed to extract proxy'; + break; + case CLIInstallErrorReason.ProxyFetch: + message = 'Failed to fetch proxy'; + break; + case CLIInstallErrorReason.CoreDirectory: + message = 'Failed to find core directory in proxy output'; + break; + case CLIInstallErrorReason.CoreInstall: + message = 'Failed to install core'; + break; + default: + message = 'An unknown error occurred'; + break; + } + + if (details != null) { + message += `: ${details}`; + } + + return message; } } From b516f1cfb19179c9fa1fbd1c4a5e885202303758 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Tue, 26 Aug 2025 15:53:39 -0700 Subject: [PATCH 06/33] Includes source and CLI version in telemetry --- docs/telemetry-events.md | 182 +++++++++++++++-------------- src/constants.telemetry.ts | 14 ++- src/env/node/gk/cli/integration.ts | 34 +++++- 3 files changed, 138 insertions(+), 92 deletions(-) diff --git a/docs/telemetry-events.md b/docs/telemetry-events.md index 91cf23be38c34..617032d918e05 100644 --- a/docs/telemetry-events.md +++ b/docs/telemetry-events.md @@ -578,7 +578,8 @@ void { 'attempts': number, 'autoInstall': boolean, - 'error.message': string + 'error.message': string, + 'source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees' } ``` @@ -589,7 +590,8 @@ void ```typescript { 'attempts': number, - 'autoInstall': boolean + 'autoInstall': boolean, + 'source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees' } ``` @@ -600,7 +602,9 @@ void ```typescript { 'attempts': number, - 'autoInstall': boolean + 'autoInstall': boolean, + 'source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'version': string } ``` @@ -829,7 +833,7 @@ or ```typescript { [`context.${string}`]: string | number | boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -852,7 +856,7 @@ or 'context.repository.id': string, 'context.repository.provider.id': string, 'context.repository.scheme': string, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -871,7 +875,7 @@ or 'context.pinned': boolean, 'context.type': 'stash' | 'commit', 'context.uncommitted': boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -884,7 +888,7 @@ or ```typescript { - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -917,7 +921,7 @@ or 'context.repository.id': string, 'context.repository.provider.id': string, 'context.repository.scheme': string, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -944,7 +948,7 @@ or 'context.pinned': boolean, 'context.type': 'stash' | 'commit', 'context.uncommitted': boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -998,10 +1002,10 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'view' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -1053,10 +1057,10 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'view' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -1108,10 +1112,10 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'view' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -1163,10 +1167,10 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'view' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -1218,10 +1222,10 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'view' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -1273,10 +1277,10 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'view' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -1328,10 +1332,10 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'view' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -1383,10 +1387,10 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'view' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -1438,10 +1442,10 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'view' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -1493,10 +1497,10 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'view' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -1548,10 +1552,10 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'view' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -1603,10 +1607,10 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'view' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -1618,7 +1622,7 @@ or ```typescript { [`context.${string}`]: string | number | boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -1670,10 +1674,10 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'view' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -1725,10 +1729,10 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'view' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -1739,7 +1743,7 @@ or ```typescript { - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -1753,7 +1757,7 @@ or ```typescript { [`context.${string}`]: string | number | boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -1807,10 +1811,10 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'view' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -1862,10 +1866,10 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'view' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -1883,7 +1887,7 @@ or 'context.repository.id': string, 'context.repository.provider.id': string, 'context.repository.scheme': string, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -1902,7 +1906,7 @@ or 'context.repository.id': string, 'context.repository.provider.id': string, 'context.repository.scheme': string, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -1921,7 +1925,7 @@ or 'context.repository.id': string, 'context.repository.provider.id': string, 'context.repository.scheme': string, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -1941,7 +1945,7 @@ or 'context.repository.id': string, 'context.repository.provider.id': string, 'context.repository.scheme': string, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -1953,7 +1957,7 @@ or ```typescript { [`context.${string}`]: string | number | boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -1975,7 +1979,7 @@ or 'context.repository.id': string, 'context.repository.provider.id': string, 'context.repository.scheme': string, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -2004,7 +2008,7 @@ or 'context.repository.id': string, 'context.repository.provider.id': string, 'context.repository.scheme': string, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -2024,7 +2028,7 @@ or 'context.repository.id': string, 'context.repository.provider.id': string, 'context.repository.scheme': string, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -2042,7 +2046,7 @@ or 'context.repository.id': string, 'context.repository.provider.id': string, 'context.repository.scheme': string, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -2065,7 +2069,7 @@ or 'context.repository.id': string, 'context.repository.provider.id': string, 'context.repository.scheme': string, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -2084,7 +2088,7 @@ or 'context.repository.id': string, 'context.repository.provider.id': string, 'context.repository.scheme': string, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -2104,7 +2108,7 @@ or 'context.repository.id': string, 'context.repository.provider.id': string, 'context.repository.scheme': string, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -2124,7 +2128,7 @@ or 'context.repository.id': string, 'context.repository.provider.id': string, 'context.repository.scheme': string, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -2142,7 +2146,7 @@ or ```typescript { - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -2193,7 +2197,7 @@ or 'context.repository.id': string, 'context.repository.provider.id': string, 'context.repository.scheme': string, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -2207,7 +2211,7 @@ or ```typescript { [`context.${string}`]: string | number | boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -2230,7 +2234,7 @@ or 'context.repository.id': string, 'context.repository.provider.id': string, 'context.repository.scheme': string, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -2249,7 +2253,7 @@ or 'context.pinned': boolean, 'context.type': 'stash' | 'commit', 'context.uncommitted': boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -2262,7 +2266,7 @@ or ```typescript { - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -2295,7 +2299,7 @@ or 'context.repository.id': string, 'context.repository.provider.id': string, 'context.repository.scheme': string, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -2322,7 +2326,7 @@ or 'context.pinned': boolean, 'context.type': 'stash' | 'commit', 'context.uncommitted': boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -2344,7 +2348,7 @@ void ```typescript { [`context.${string}`]: string | number | boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -2393,7 +2397,7 @@ void ```typescript { - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -2407,7 +2411,7 @@ void ```typescript { [`context.${string}`]: string | number | boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -2766,7 +2770,9 @@ void ```typescript { - 'requiresUserCompletion': boolean + 'cli.version': string, + 'requiresUserCompletion': boolean, + 'source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees' } ``` @@ -2776,8 +2782,10 @@ void ```typescript { + 'cli.version': string, 'error.message': string, - 'reason': string + 'reason': string, + 'source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees' } ``` @@ -2786,7 +2794,9 @@ void > Sent when GitKraken MCP setup is started ```typescript -void +{ + 'source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees' +} ``` ### openReviewMode @@ -2801,7 +2811,7 @@ void 'repoPrivacy': 'private' | 'public' | 'local', 'repository.visibility': 'private' | 'public' | 'local', // Provided for compatibility with other GK surfaces - 'source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'view' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'walkthrough' | 'whatsnew' | 'worktrees' + 'source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees' } ``` @@ -2810,7 +2820,7 @@ void ```typescript { [`context.${string}`]: string | number | boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -2821,7 +2831,7 @@ void ```typescript { - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -2835,7 +2845,7 @@ void ```typescript { [`context.${string}`]: string | number | boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -2967,7 +2977,7 @@ void ```typescript { [`context.${string}`]: string | number | boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -2978,7 +2988,7 @@ void ```typescript { - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -2992,7 +3002,7 @@ void ```typescript { [`context.${string}`]: string | number | boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -3253,7 +3263,7 @@ or 'context.scope.type': 'file' | 'folder' | 'repo', 'context.showAllBranches': boolean, 'context.sliceBy': 'branch' | 'author', - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -3268,7 +3278,7 @@ or ```typescript { [`context.${string}`]: string | number | boolean, - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -3287,7 +3297,7 @@ or 'context.scope.type': 'file' | 'folder' | 'repo', 'context.showAllBranches': boolean, 'context.sliceBy': 'branch' | 'author', - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -3306,7 +3316,7 @@ or 'context.scope.type': 'file' | 'folder' | 'repo', 'context.showAllBranches': boolean, 'context.sliceBy': 'branch' | 'author', - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -3328,7 +3338,7 @@ or 'context.scope.type': 'file' | 'folder' | 'repo', 'context.showAllBranches': boolean, 'context.sliceBy': 'branch' | 'author', - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -3347,7 +3357,7 @@ or 'context.scope.type': 'file' | 'folder' | 'repo', 'context.showAllBranches': boolean, 'context.sliceBy': 'branch' | 'author', - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string @@ -3358,7 +3368,7 @@ or ```typescript { - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, @@ -3381,7 +3391,7 @@ or 'context.scope.type': 'file' | 'folder' | 'repo', 'context.showAllBranches': boolean, 'context.sliceBy': 'branch' | 'author', - 'context.webview.host': 'editor' | 'view', + 'context.webview.host': 'view' | 'editor', 'context.webview.id': string, 'context.webview.instanceId': string, 'context.webview.type': string, diff --git a/src/constants.telemetry.ts b/src/constants.telemetry.ts index defbe0b0a5877..9f2be7120d463 100644 --- a/src/constants.telemetry.ts +++ b/src/constants.telemetry.ts @@ -246,7 +246,7 @@ export interface TelemetryEvents extends WebviewShowAbortedEvents, WebviewShownE 'launchpad/operation/slow': LaunchpadOperationSlowEvent; /** Sent when GitKraken MCP setup is started */ - 'mcp/setup/started': void; + 'mcp/setup/started': MCPSetupStartedEvent; /** Sent when GitKraken MCP setup is completed */ 'mcp/setup/completed': MCPSetupCompletedEvent; /** Sent when GitKraken MCP setup fails */ @@ -487,6 +487,7 @@ export interface AIFeedbackEvent extends AIEventDataBase { } export interface CLIInstallStartedEvent { + source?: Sources; autoInstall: boolean; attempts: number; } @@ -494,20 +495,31 @@ export interface CLIInstallStartedEvent { export interface CLIInstallSucceededEvent { autoInstall: boolean; attempts: number; + source?: Sources; + version?: string; } export interface CLIInstallFailedEvent { autoInstall: boolean; attempts: number; 'error.message'?: string; + source?: Sources; +} + +export interface MCPSetupStartedEvent { + source: Sources; } export interface MCPSetupCompletedEvent { + source: Sources; + 'cli.version'?: string; requiresUserCompletion: boolean; } export interface MCPSetupFailedEvent { + source: Sources; reason: string; + 'cli.version'?: string; 'error.message'?: string; } diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index cf1c3113f146c..c83fe4e6c81f4 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -1,6 +1,7 @@ import { arch } from 'process'; import type { ConfigurationChangeEvent } from 'vscode'; import { version as codeVersion, Disposable, env, ProgressLocation, Uri, window, workspace } from 'vscode'; +import type { Source, Sources } from '../../../../constants.telemetry'; import type { Container } from '../../../../container'; import type { SubscriptionChangeEvent } from '../../../../plus/gk/subscriptionService'; import { registerCommand } from '../../../../system/-webview/command'; @@ -89,10 +90,12 @@ export class GkCliIntegrationProvider implements Disposable { } @gate() - private async setupMCP(): Promise { + private async setupMCP(source?: Sources): Promise { + const commandSource = source ?? 'commandPalette'; const scope = getLogScope(); + let cliVersion: string | undefined; if (this.container.telemetry.enabled) { - this.container.telemetry.sendEvent('mcp/setup/started'); + this.container.telemetry.sendEvent('mcp/setup/started', { source: commandSource }); } try { @@ -104,6 +107,7 @@ export class GkCliIntegrationProvider implements Disposable { if (this.container.telemetry.enabled) { this.container.telemetry.sendEvent('mcp/setup/failed', { reason: 'unsupported vscode version', + source: commandSource, }); } return; @@ -127,6 +131,7 @@ export class GkCliIntegrationProvider implements Disposable { if (this.container.telemetry.enabled) { this.container.telemetry.sendEvent('mcp/setup/failed', { reason: 'unsupported app', + source: commandSource, }); } return; @@ -154,7 +159,7 @@ export class GkCliIntegrationProvider implements Disposable { cancellable: false, }, async () => { - await this.installCLI(); + cliVersion = await this.installCLI(); }, ); } catch (ex) { @@ -193,6 +198,7 @@ export class GkCliIntegrationProvider implements Disposable { this.container.telemetry.sendEvent('mcp/setup/failed', { reason: failureReason, 'error.message': ex instanceof Error ? ex.message : 'Unknown error during installation', + source: commandSource, }); } } @@ -206,6 +212,8 @@ export class GkCliIntegrationProvider implements Disposable { this.container.telemetry.sendEvent('mcp/setup/failed', { reason: 'unknown error', 'error.message': 'Unknown error during installation', + source: commandSource, + 'cli.version': cliVersion, }); } return; @@ -222,6 +230,8 @@ export class GkCliIntegrationProvider implements Disposable { if (this.container.telemetry.enabled) { this.container.telemetry.sendEvent('mcp/setup/failed', { reason: 'user cancelled', + source: commandSource, + 'cli.version': cliVersion, }); } return; @@ -248,6 +258,8 @@ export class GkCliIntegrationProvider implements Disposable { this.container.telemetry.sendEvent('mcp/setup/failed', { reason: 'unexpected output from mcp install command', 'error.message': `Unexpected output from mcp install command: ${output}`, + source: commandSource, + 'cli.version': cliVersion, }); } Logger.error(`Unexpected output from mcp install command: ${output}`, scope); @@ -260,6 +272,8 @@ export class GkCliIntegrationProvider implements Disposable { this.container.telemetry.sendEvent('mcp/setup/completed', { requiresUserCompletion: appName === 'cursor' || appName === 'vscode' || appName === 'vscode-insiders', + source: commandSource, + 'cli.version': cliVersion, }); } } catch (ex) { @@ -268,6 +282,8 @@ export class GkCliIntegrationProvider implements Disposable { this.container.telemetry.sendEvent('mcp/setup/failed', { reason: 'unknown error', 'error.message': ex instanceof Error ? ex.message : 'Unknown error during installation', + source: commandSource, + 'cli.version': cliVersion, }); } @@ -278,14 +294,16 @@ export class GkCliIntegrationProvider implements Disposable { } @gate() - private async installCLI(autoInstall?: boolean): Promise { + private async installCLI(autoInstall?: boolean, source?: Sources): Promise { let attempts = 0; + let cliVersion: string | undefined; try { const cliInstall = this.container.storage.get('gk:cli:install'); attempts = cliInstall?.attempts ?? 0; attempts += 1; if (this.container.telemetry.enabled) { this.container.telemetry.sendEvent('cli/install/started', { + source: source, autoInstall: autoInstall ?? false, attempts: attempts, }); @@ -377,6 +395,7 @@ export class GkCliIntegrationProvider implements Disposable { const cliZipArchiveDownloadInfo: { version?: string; packages?: { zip?: string } } | undefined = (await response.json()) as any; downloadUrl = cliZipArchiveDownloadInfo?.packages?.zip; + cliVersion = cliZipArchiveDownloadInfo?.version; } catch (ex) { throw new CLIInstallError( CLIInstallErrorReason.ProxyUrlFormat, @@ -491,6 +510,8 @@ export class GkCliIntegrationProvider implements Disposable { this.container.telemetry.sendEvent('cli/install/succeeded', { autoInstall: autoInstall ?? false, attempts: attempts, + source: source, + version: cliVersion, }); } await this.authCLI(); @@ -527,12 +548,15 @@ export class GkCliIntegrationProvider implements Disposable { autoInstall: autoInstall ?? false, attempts: attempts, 'error.message': ex instanceof Error ? ex.message : 'Unknown error', + source: source, }); } if (!autoInstall) { throw ex; } } + + return cliVersion; } private async runCLICommand( @@ -576,7 +600,7 @@ export class GkCliIntegrationProvider implements Disposable { } private registerCommands(): Disposable[] { - return [registerCommand('gitlens.ai.mcp.install', () => this.setupMCP())]; + return [registerCommand('gitlens.ai.mcp.install', (src?: Source) => this.setupMCP(src?.source))]; } } From 298e760f02cd5b65eb1fe7190e650b31401f1f37 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Tue, 26 Aug 2025 16:21:36 -0700 Subject: [PATCH 07/33] Uses built-in app name function and always passes to mcp install command --- src/env/node/gk/cli/integration.ts | 62 +++++++++++++++++------------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index c83fe4e6c81f4..e68f23fd216b7 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -6,6 +6,7 @@ import type { Container } from '../../../../container'; import type { SubscriptionChangeEvent } from '../../../../plus/gk/subscriptionService'; import { registerCommand } from '../../../../system/-webview/command'; import { configuration } from '../../../../system/-webview/configuration'; +import { getHostAppName } from '../../../../system/-webview/vscode'; import { openUrl } from '../../../../system/-webview/vscode/uris'; import { gate } from '../../../../system/decorators/gate'; import { Logger } from '../../../../system/logger'; @@ -97,10 +98,21 @@ export class GkCliIntegrationProvider implements Disposable { if (this.container.telemetry.enabled) { this.container.telemetry.sendEvent('mcp/setup/started', { source: commandSource }); } + const appName = toMcpInstallProvider(await getHostAppName()); + if (appName == null) { + void window.showInformationMessage(`Failed to install MCP integration: Could not determine app name`); + if (this.container.telemetry.enabled) { + this.container.telemetry.sendEvent('mcp/setup/failed', { + reason: 'no app name', + source: commandSource, + }); + } + return; + } try { if ( - (env.appName === 'Visual Studio Code' || env.appName === 'Visual Studio Code - Insiders') && + (appName === 'vscode' || appName === 'vscode-insiders' || appName === 'vscode-exploration') && compare(codeVersion, '1.102') < 0 ) { void window.showInformationMessage('Use of this command requires VS Code 1.102 or later.'); @@ -113,31 +125,6 @@ export class GkCliIntegrationProvider implements Disposable { return; } - let appName = 'vscode'; - switch (env.appName) { - case 'Visual Studio Code': - break; - case 'Visual Studio Code - Insiders': - appName = 'vscode-insiders'; - break; - case 'Cursor': - appName = 'cursor'; - break; - case 'Windsurf': - appName = 'windsurf'; - break; - default: { - void window.showInformationMessage(`MCP installation is not supported for app: ${env.appName}`); - if (this.container.telemetry.enabled) { - this.container.telemetry.sendEvent('mcp/setup/failed', { - reason: 'unsupported app', - source: commandSource, - }); - } - return; - } - } - let cliInstall = this.container.storage.get('gk:cli:install'); let cliPath = this.container.storage.get('gk:cli:path'); let cliProxyFileExists = true; @@ -248,6 +235,16 @@ export class GkCliIntegrationProvider implements Disposable { output = output.trim(); if (output === 'GitKraken MCP Server Successfully Installed!') { return; + } else if (output.includes('not a supported MCP client')) { + if (this.container.telemetry.enabled) { + this.container.telemetry.sendEvent('mcp/setup/failed', { + reason: 'unsupported app', + 'error.message': `Not a supported MCP client: ${appName}`, + source: commandSource, + 'cli.version': cliVersion, + }); + } + return; } // Check if the output is a valid url. If so, run it @@ -662,3 +659,16 @@ class CLIInstallError extends Error { return message; } } + +function toMcpInstallProvider(appHostName: string | undefined): string | undefined { + switch (appHostName) { + case 'code': + return 'vscode'; + case 'code-insiders': + return 'vscode-insiders'; + case 'code-exploration': + return 'vscode-exploration'; + default: + return appHostName; + } +} From 747e708c677f4e7b7abcdbdd81f44018f19203db Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Tue, 26 Aug 2025 22:15:25 -0700 Subject: [PATCH 08/33] Improvements from feedback --- src/constants.storage.ts | 2 +- src/env/node/gk/cli/integration.ts | 188 +++++++++++++++++------------ 2 files changed, 111 insertions(+), 79 deletions(-) diff --git a/src/constants.storage.ts b/src/constants.storage.ts index ae6f0047c7c34..8e06d377cae3a 100644 --- a/src/constants.storage.ts +++ b/src/constants.storage.ts @@ -83,7 +83,7 @@ export type GlobalStorage = { // Value based on `currentOnboardingVersion` in composer's protocol 'composer:onboarding:dismissed': string; 'composer:onboarding:stepReached': number; - 'gk:cli:install': { status: 'attempted' | 'unsupported' | 'completed'; attempts: number }; + 'gk:cli:install': { status: 'attempted' | 'unsupported' | 'completed'; attempts: number; version?: string }; 'gk:cli:corePath': string; 'gk:cli:path': string; 'home:sections:collapsed': string[]; diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index e68f23fd216b7..d4f67b1446bee 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -98,6 +98,18 @@ export class GkCliIntegrationProvider implements Disposable { if (this.container.telemetry.enabled) { this.container.telemetry.sendEvent('mcp/setup/started', { source: commandSource }); } + + if (isWeb) { + void window.showErrorMessage('GitKraken MCP installation is not supported on this platform.'); + if (this.container.telemetry.enabled) { + this.container.telemetry.sendEvent('mcp/setup/failed', { + reason: 'web environment unsupported', + source: commandSource, + }); + } + return; + } + const appName = toMcpInstallProvider(await getHostAppName()); if (appName == null) { void window.showInformationMessage(`Failed to install MCP integration: Could not determine app name`); @@ -125,80 +137,74 @@ export class GkCliIntegrationProvider implements Disposable { return; } - let cliInstall = this.container.storage.get('gk:cli:install'); - let cliPath = this.container.storage.get('gk:cli:path'); - let cliProxyFileExists = true; - if (cliPath != null) { - try { - await workspace.fs.stat( - Uri.joinPath(Uri.file(cliPath), getPlatform() === 'windows' ? 'gk.exe' : 'gk'), - ); - } catch { - cliProxyFileExists = false; - } - } - if (cliInstall?.status !== 'completed' || cliPath == null || !cliProxyFileExists) { - try { - await window.withProgress( - { - location: ProgressLocation.Notification, - title: 'Setting up MCP integration...', - cancellable: false, - }, - async () => { - cliVersion = await this.installCLI(); - }, - ); - } catch (ex) { - let failureReason = 'unknown error'; - if (ex instanceof CLIInstallError) { - switch (ex.reason) { - case CLIInstallErrorReason.WebEnvironmentUnsupported: - void window.showErrorMessage( - 'MCP installation is not supported in the web environment.', - ); - failureReason = 'web environment unsupported'; - break; - case CLIInstallErrorReason.UnsupportedPlatform: - void window.showErrorMessage('MCP installation is not supported on this platform.'); - failureReason = 'unsupported platform'; - break; - case CLIInstallErrorReason.ProxyUrlFetch: - case CLIInstallErrorReason.ProxyUrlFormat: - case CLIInstallErrorReason.ProxyFetch: - case CLIInstallErrorReason.ProxyDownload: - case CLIInstallErrorReason.ProxyExtract: - case CLIInstallErrorReason.CoreDirectory: - case CLIInstallErrorReason.CoreInstall: - void window.showErrorMessage('Failed to install MCP server locally.'); - failureReason = 'local installation failed'; - break; - default: - void window.showErrorMessage( - `Failed to install MCP integration: ${ex instanceof Error ? ex.message : 'Unknown error during installation'}`, - ); - break; - } + let cliVersion: string | undefined; + let cliPath: string | undefined; + try { + await window.withProgress( + { + location: ProgressLocation.Notification, + title: 'Setting up the GitKraken MCP...', + cancellable: false, + }, + async () => { + const { cliVersion: installedVersion, cliPath: installedPath } = await this.installCLI( + false, + source, + ); + cliVersion = installedVersion; + cliPath = installedPath; + }, + ); + } catch (ex) { + let failureReason = 'unknown error'; + if (ex instanceof CLIInstallError) { + switch (ex.reason) { + case CLIInstallErrorReason.WebEnvironmentUnsupported: + void window.showErrorMessage( + 'GitKraken MCP installation is not supported on this platform.', + ); + failureReason = 'web environment unsupported'; + break; + case CLIInstallErrorReason.UnsupportedPlatform: + void window.showErrorMessage( + 'GitKraken MCP installation is not supported on this platform.', + ); + failureReason = 'unsupported platform'; + break; + case CLIInstallErrorReason.ProxyUrlFetch: + case CLIInstallErrorReason.ProxyUrlFormat: + case CLIInstallErrorReason.ProxyFetch: + case CLIInstallErrorReason.ProxyDownload: + case CLIInstallErrorReason.ProxyExtract: + case CLIInstallErrorReason.CoreDirectory: + case CLIInstallErrorReason.CoreInstall: + void window.showErrorMessage('Failed to install the GitKraken MCP server locally.'); + failureReason = 'local installation failed'; + break; + default: + void window.showErrorMessage( + `Failed to install the GitKraken MCP integration: ${ex instanceof Error ? ex.message : 'Unknown error.'}`, + ); + break; } + } - if (this.container.telemetry.enabled) { - this.container.telemetry.sendEvent('mcp/setup/failed', { - reason: failureReason, - 'error.message': ex instanceof Error ? ex.message : 'Unknown error during installation', - source: commandSource, - }); - } + if (this.container.telemetry.enabled) { + this.container.telemetry.sendEvent('mcp/setup/failed', { + reason: failureReason, + 'error.message': ex instanceof Error ? ex.message : 'Unknown error', + source: commandSource, + }); } + return; } - cliInstall = this.container.storage.get('gk:cli:install'); - cliPath = this.container.storage.get('gk:cli:path'); - if (cliInstall?.status !== 'completed' || cliPath == null) { - void window.showErrorMessage('Failed to install MCP integration: Unknown error during installation.'); + if (cliPath == null) { + void window.showErrorMessage('Failed to install the GitKraken MCP: Unknown error.'); if (this.container.telemetry.enabled) { this.container.telemetry.sendEvent('mcp/setup/failed', { reason: 'unknown error', - 'error.message': 'Unknown error during installation', + 'error.message': 'Unknown error', source: commandSource, 'cli.version': cliVersion, }); @@ -208,7 +214,7 @@ export class GkCliIntegrationProvider implements Disposable { if (appName !== 'cursor' && appName !== 'vscode' && appName !== 'vscode-insiders') { const confirmation = await window.showInformationMessage( - `MCP configured successfully. Click 'Finish' to add it to your MCP server list and complete the installation.`, + `GitKraken MCP installed successfully. Click 'Finish' to add it to your MCP server list and complete the setup.`, { modal: true }, { title: 'Finish' }, { title: 'Cancel', isCloseAffordance: true }, @@ -234,6 +240,13 @@ export class GkCliIntegrationProvider implements Disposable { output = output.trim(); if (output === 'GitKraken MCP Server Successfully Installed!') { + if (this.container.telemetry.enabled) { + this.container.telemetry.sendEvent('mcp/setup/completed', { + requiresUserCompletion: false, + source: commandSource, + 'cli.version': cliVersion, + }); + } return; } else if (output.includes('not a supported MCP client')) { if (this.container.telemetry.enabled) { @@ -260,15 +273,14 @@ export class GkCliIntegrationProvider implements Disposable { }); } Logger.error(`Unexpected output from mcp install command: ${output}`, scope); - void window.showErrorMessage(`Failed to install MCP integration: error getting install URL`); + void window.showErrorMessage(`Failed to install the GitKrakenMCP integration: unknown error`); return; } await openUrl(output); if (this.container.telemetry.enabled) { this.container.telemetry.sendEvent('mcp/setup/completed', { - requiresUserCompletion: - appName === 'cursor' || appName === 'vscode' || appName === 'vscode-insiders', + requiresUserCompletion: true, source: commandSource, 'cli.version': cliVersion, }); @@ -278,24 +290,41 @@ export class GkCliIntegrationProvider implements Disposable { if (this.container.telemetry.enabled) { this.container.telemetry.sendEvent('mcp/setup/failed', { reason: 'unknown error', - 'error.message': ex instanceof Error ? ex.message : 'Unknown error during installation', + 'error.message': ex instanceof Error ? ex.message : 'Unknown error', source: commandSource, 'cli.version': cliVersion, }); } void window.showErrorMessage( - `Failed to install MCP integration: ${ex instanceof Error ? ex.message : String(ex)}`, + `Failed to install the GitKraken MCP integration: ${ex instanceof Error ? ex.message : 'Unknown error'}`, ); } } @gate() - private async installCLI(autoInstall?: boolean, source?: Sources): Promise { + private async installCLI( + autoInstall?: boolean, + source?: Sources, + ): Promise<{ cliVersion?: string; cliPath?: string }> { let attempts = 0; let cliVersion: string | undefined; + let cliPath: string | undefined; + const cliInstall = this.container.storage.get('gk:cli:install'); + if (autoInstall) { + if (cliInstall?.status === 'completed') { + cliVersion = cliInstall.version; + cliPath = this.container.storage.get('gk:cli:path'); + return { cliVersion: cliVersion, cliPath: cliPath }; + } else if ( + cliInstall?.status === 'unsupported' || + (cliInstall?.status === 'attempted' && cliInstall.attempts >= 5) + ) { + return { cliVersion: undefined, cliPath: undefined }; + } + } + try { - const cliInstall = this.container.storage.get('gk:cli:install'); attempts = cliInstall?.attempts ?? 0; attempts += 1; if (this.container.telemetry.enabled) { @@ -456,7 +485,7 @@ export class GkCliIntegrationProvider implements Disposable { // Use the run function to extract the installer file from the installer zip if (platform === 'windows') { // On Windows, use PowerShell to extract the zip file. - // Force overwrite if the file already exists and the force param is true + // Force overwrite if the file already exists with -Force await run( 'powershell.exe', [ @@ -466,7 +495,7 @@ export class GkCliIntegrationProvider implements Disposable { 'utf8', ); } else { - // On Unix-like systems, use the unzip command to extract the zip file + // On Unix-like systems, use the unzip command to extract the zip file, forcing overwrite with -o await run('unzip', ['-o', cliProxyZipFilePath.fsPath, '-d', globalStoragePath.fsPath], 'utf8'); } @@ -475,8 +504,11 @@ export class GkCliIntegrationProvider implements Disposable { globalStoragePath, platform === 'windows' ? 'gk.exe' : 'gk', ); + + // This will throw if the file doesn't exist await workspace.fs.stat(cliExtractedProxyFilePath); void this.container.storage.store('gk:cli:path', globalStoragePath.fsPath).catch(); + cliPath = globalStoragePath.fsPath; } catch (ex) { throw new CLIInstallError( CLIInstallErrorReason.ProxyExtract, @@ -501,7 +533,7 @@ export class GkCliIntegrationProvider implements Disposable { Logger.log('CLI install completed.'); void this.container.storage - .store('gk:cli:install', { status: 'completed', attempts: attempts }) + .store('gk:cli:install', { status: 'completed', attempts: attempts, version: cliVersion }) .catch(); if (this.container.telemetry.enabled) { this.container.telemetry.sendEvent('cli/install/succeeded', { @@ -553,7 +585,7 @@ export class GkCliIntegrationProvider implements Disposable { } } - return cliVersion; + return { cliVersion: cliVersion, cliPath: cliPath }; } private async runCLICommand( From f44e4c094cfc11fe03b609d165b27a1815ccf688 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Wed, 27 Aug 2025 08:43:44 -0700 Subject: [PATCH 09/33] Sends status from installCLI --- src/env/node/gk/cli/integration.ts | 83 +++++++++++++++--------------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index d4f67b1446bee..511398ac0c787 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -19,7 +19,6 @@ import type { IpcServer } from './ipcServer'; import { createIpcServer } from './ipcServer'; const enum CLIInstallErrorReason { - WebEnvironmentUnsupported, UnsupportedPlatform, ProxyUrlFetch, ProxyUrlFormat, @@ -147,10 +146,16 @@ export class GkCliIntegrationProvider implements Disposable { cancellable: false, }, async () => { - const { cliVersion: installedVersion, cliPath: installedPath } = await this.installCLI( - false, - source, - ); + const { + cliVersion: installedVersion, + cliPath: installedPath, + status, + } = await this.installCLI(false, source); + if (status === 'unsupported') { + throw new CLIInstallError(CLIInstallErrorReason.UnsupportedPlatform); + } else if (status === 'attempted') { + throw new CLIInstallError(CLIInstallErrorReason.CoreInstall); + } cliVersion = installedVersion; cliPath = installedPath; }, @@ -159,12 +164,6 @@ export class GkCliIntegrationProvider implements Disposable { let failureReason = 'unknown error'; if (ex instanceof CLIInstallError) { switch (ex.reason) { - case CLIInstallErrorReason.WebEnvironmentUnsupported: - void window.showErrorMessage( - 'GitKraken MCP installation is not supported on this platform.', - ); - failureReason = 'web environment unsupported'; - break; case CLIInstallErrorReason.UnsupportedPlatform: void window.showErrorMessage( 'GitKraken MCP installation is not supported on this platform.', @@ -306,38 +305,35 @@ export class GkCliIntegrationProvider implements Disposable { private async installCLI( autoInstall?: boolean, source?: Sources, - ): Promise<{ cliVersion?: string; cliPath?: string }> { - let attempts = 0; - let cliVersion: string | undefined; - let cliPath: string | undefined; + ): Promise<{ cliVersion?: string; cliPath?: string; status: 'completed' | 'unsupported' | 'attempted' }> { const cliInstall = this.container.storage.get('gk:cli:install'); - if (autoInstall) { - if (cliInstall?.status === 'completed') { - cliVersion = cliInstall.version; - cliPath = this.container.storage.get('gk:cli:path'); - return { cliVersion: cliVersion, cliPath: cliPath }; - } else if ( - cliInstall?.status === 'unsupported' || - (cliInstall?.status === 'attempted' && cliInstall.attempts >= 5) - ) { - return { cliVersion: undefined, cliPath: undefined }; - } + let cliInstallAttempts = cliInstall?.attempts ?? 0; + let cliInstallStatus = cliInstall?.status ?? 'attempted'; + let cliVersion = cliInstall?.version; + let cliPath = this.container.storage.get('gk:cli:path'); + + if (cliInstallStatus === 'completed') { + cliVersion = cliInstall?.version; + return { cliVersion: cliVersion, cliPath: cliPath, status: 'completed' }; + } else if (cliInstallStatus === 'unsupported') { + return { cliVersion: undefined, cliPath: undefined, status: 'unsupported' }; + } else if (autoInstall && cliInstallStatus === 'attempted' && cliInstallAttempts >= 5) { + return { cliVersion: undefined, cliPath: undefined, status: 'attempted' }; } try { - attempts = cliInstall?.attempts ?? 0; - attempts += 1; + cliInstallAttempts += 1; if (this.container.telemetry.enabled) { this.container.telemetry.sendEvent('cli/install/started', { source: source, autoInstall: autoInstall ?? false, - attempts: attempts, + attempts: cliInstallAttempts, }); } void this.container.storage .store('gk:cli:install', { status: 'attempted', - attempts: attempts, + attempts: cliInstallAttempts, }) .catch(); @@ -345,11 +341,11 @@ export class GkCliIntegrationProvider implements Disposable { void this.container.storage .store('gk:cli:install', { status: 'unsupported', - attempts: attempts, + attempts: cliInstallAttempts, }) .catch(); - throw new CLIInstallError(CLIInstallErrorReason.WebEnvironmentUnsupported); + throw new CLIInstallError(CLIInstallErrorReason.UnsupportedPlatform, undefined, 'web'); } // Detect platform and architecture @@ -385,7 +381,7 @@ export class GkCliIntegrationProvider implements Disposable { void this.container.storage .store('gk:cli:install', { status: 'unsupported', - attempts: attempts, + attempts: cliInstallAttempts, }) .catch(); throw new CLIInstallError(CLIInstallErrorReason.UnsupportedPlatform, undefined, platform); @@ -532,13 +528,18 @@ export class GkCliIntegrationProvider implements Disposable { } Logger.log('CLI install completed.'); + cliInstallStatus = 'completed'; void this.container.storage - .store('gk:cli:install', { status: 'completed', attempts: attempts, version: cliVersion }) + .store('gk:cli:install', { + status: cliInstallStatus, + attempts: cliInstallAttempts, + version: cliVersion, + }) .catch(); if (this.container.telemetry.enabled) { this.container.telemetry.sendEvent('cli/install/succeeded', { autoInstall: autoInstall ?? false, - attempts: attempts, + attempts: cliInstallAttempts, source: source, version: cliVersion, }); @@ -575,17 +576,20 @@ export class GkCliIntegrationProvider implements Disposable { if (this.container.telemetry.enabled) { this.container.telemetry.sendEvent('cli/install/failed', { autoInstall: autoInstall ?? false, - attempts: attempts, + attempts: cliInstallAttempts, 'error.message': ex instanceof Error ? ex.message : 'Unknown error', source: source, }); } - if (!autoInstall) { + + if (CLIInstallError.is(ex, CLIInstallErrorReason.UnsupportedPlatform)) { + cliInstallStatus = 'unsupported'; + } else if (!autoInstall) { throw ex; } } - return { cliVersion: cliVersion, cliPath: cliPath }; + return { cliVersion: cliVersion, cliPath: cliPath, status: cliInstallStatus }; } private async runCLICommand( @@ -652,9 +656,6 @@ class CLIInstallError extends Error { private static buildErrorMessage(reason: CLIInstallErrorReason, details?: string): string { let message; switch (reason) { - case CLIInstallErrorReason.WebEnvironmentUnsupported: - message = 'Web environment is not supported'; - break; case CLIInstallErrorReason.UnsupportedPlatform: message = 'Unsupported platform'; break; From 72c5b99b69171a77dd6c4594a0e6285637e10d7d Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Wed, 27 Aug 2025 08:59:30 -0700 Subject: [PATCH 10/33] Install command always verifies path existence to proxy --- src/env/node/gk/cli/integration.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index 511398ac0c787..bc5beca630d2c 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -311,10 +311,22 @@ export class GkCliIntegrationProvider implements Disposable { let cliInstallStatus = cliInstall?.status ?? 'attempted'; let cliVersion = cliInstall?.version; let cliPath = this.container.storage.get('gk:cli:path'); + const platform = getPlatform(); if (cliInstallStatus === 'completed') { - cliVersion = cliInstall?.version; - return { cliVersion: cliVersion, cliPath: cliPath, status: 'completed' }; + if (cliPath == null) { + cliInstallStatus = 'attempted'; + cliVersion = undefined; + } else { + cliVersion = cliInstall?.version; + try { + await workspace.fs.stat(Uri.joinPath(Uri.file(cliPath), platform === 'windows' ? 'gk.exe' : 'gk')); + return { cliVersion: cliVersion, cliPath: cliPath, status: 'completed' }; + } catch { + cliInstallStatus = 'attempted'; + cliVersion = undefined; + } + } } else if (cliInstallStatus === 'unsupported') { return { cliVersion: undefined, cliPath: undefined, status: 'unsupported' }; } else if (autoInstall && cliInstallStatus === 'attempted' && cliInstallAttempts >= 5) { @@ -348,9 +360,6 @@ export class GkCliIntegrationProvider implements Disposable { throw new CLIInstallError(CLIInstallErrorReason.UnsupportedPlatform, undefined, 'web'); } - // Detect platform and architecture - const platform = getPlatform(); - // Map platform names for the API and get architecture let platformName: string; let architecture: string; From b055ea14d5e58fef70aea825c9de7567fc037f19 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Wed, 27 Aug 2025 09:05:53 -0700 Subject: [PATCH 11/33] Fixes wording inconsistencies on error messages --- src/env/node/gk/cli/integration.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index bc5beca630d2c..a2f6ded74d79b 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -111,7 +111,7 @@ export class GkCliIntegrationProvider implements Disposable { const appName = toMcpInstallProvider(await getHostAppName()); if (appName == null) { - void window.showInformationMessage(`Failed to install MCP integration: Could not determine app name`); + void window.showInformationMessage(`Failed to install the GitKraken MCP: Could not determine app name`); if (this.container.telemetry.enabled) { this.container.telemetry.sendEvent('mcp/setup/failed', { reason: 'no app name', @@ -182,7 +182,7 @@ export class GkCliIntegrationProvider implements Disposable { break; default: void window.showErrorMessage( - `Failed to install the GitKraken MCP integration: ${ex instanceof Error ? ex.message : 'Unknown error.'}`, + `Failed to install the GitKraken MCP: ${ex instanceof Error ? ex.message : 'Unknown error.'}`, ); break; } @@ -272,7 +272,7 @@ export class GkCliIntegrationProvider implements Disposable { }); } Logger.error(`Unexpected output from mcp install command: ${output}`, scope); - void window.showErrorMessage(`Failed to install the GitKrakenMCP integration: unknown error`); + void window.showErrorMessage(`Failed to install the GitKraken MCP: unknown error`); return; } @@ -296,7 +296,7 @@ export class GkCliIntegrationProvider implements Disposable { } void window.showErrorMessage( - `Failed to install the GitKraken MCP integration: ${ex instanceof Error ? ex.message : 'Unknown error'}`, + `Failed to install the GitKraken MCP: ${ex instanceof Error ? ex.message : 'Unknown error'}`, ); } } From 7583dea67fb84de63f63446f0fee1b7f6f040524 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Wed, 27 Aug 2025 09:08:45 -0700 Subject: [PATCH 12/33] install -> setup --- src/env/node/gk/cli/integration.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index a2f6ded74d79b..9a36c5b763b9d 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -99,7 +99,7 @@ export class GkCliIntegrationProvider implements Disposable { } if (isWeb) { - void window.showErrorMessage('GitKraken MCP installation is not supported on this platform.'); + void window.showErrorMessage('GitKraken MCP setup is not supported on this platform.'); if (this.container.telemetry.enabled) { this.container.telemetry.sendEvent('mcp/setup/failed', { reason: 'web environment unsupported', @@ -111,7 +111,7 @@ export class GkCliIntegrationProvider implements Disposable { const appName = toMcpInstallProvider(await getHostAppName()); if (appName == null) { - void window.showInformationMessage(`Failed to install the GitKraken MCP: Could not determine app name`); + void window.showInformationMessage(`Failed to setup the GitKraken MCP: Could not determine app name`); if (this.container.telemetry.enabled) { this.container.telemetry.sendEvent('mcp/setup/failed', { reason: 'no app name', @@ -165,9 +165,7 @@ export class GkCliIntegrationProvider implements Disposable { if (ex instanceof CLIInstallError) { switch (ex.reason) { case CLIInstallErrorReason.UnsupportedPlatform: - void window.showErrorMessage( - 'GitKraken MCP installation is not supported on this platform.', - ); + void window.showErrorMessage('GitKraken MCP setup is not supported on this platform.'); failureReason = 'unsupported platform'; break; case CLIInstallErrorReason.ProxyUrlFetch: @@ -182,7 +180,7 @@ export class GkCliIntegrationProvider implements Disposable { break; default: void window.showErrorMessage( - `Failed to install the GitKraken MCP: ${ex instanceof Error ? ex.message : 'Unknown error.'}`, + `Failed to setup the GitKraken MCP: ${ex instanceof Error ? ex.message : 'Unknown error.'}`, ); break; } @@ -199,7 +197,7 @@ export class GkCliIntegrationProvider implements Disposable { } if (cliPath == null) { - void window.showErrorMessage('Failed to install the GitKraken MCP: Unknown error.'); + void window.showErrorMessage('Failed to setup the GitKraken MCP: Unknown error.'); if (this.container.telemetry.enabled) { this.container.telemetry.sendEvent('mcp/setup/failed', { reason: 'unknown error', @@ -272,7 +270,7 @@ export class GkCliIntegrationProvider implements Disposable { }); } Logger.error(`Unexpected output from mcp install command: ${output}`, scope); - void window.showErrorMessage(`Failed to install the GitKraken MCP: unknown error`); + void window.showErrorMessage(`Failed to setup the GitKraken MCP: unknown error`); return; } @@ -296,7 +294,7 @@ export class GkCliIntegrationProvider implements Disposable { } void window.showErrorMessage( - `Failed to install the GitKraken MCP: ${ex instanceof Error ? ex.message : 'Unknown error'}`, + `Failed to setup the GitKraken MCP: ${ex instanceof Error ? ex.message : 'Unknown error'}`, ); } } From 60d468ac6d550b77775e2bd4ac1686d93f761cb6 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Wed, 27 Aug 2025 12:28:31 -0700 Subject: [PATCH 13/33] Leaves out auto-install (for now) --- src/env/node/gk/cli/integration.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index 9a36c5b763b9d..987cb859787a7 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -49,10 +49,11 @@ export class GkCliIntegrationProvider implements Disposable { this.onConfigurationChanged(); - const cliInstall = this.container.storage.get('gk:cli:install'); + // TODO: Uncomment this once we feel confident enough that the install process is stable cross-platform + /* const cliInstall = this.container.storage.get('gk:cli:install'); if (!cliInstall || (cliInstall.status === 'attempted' && cliInstall.attempts < 5)) { setTimeout(() => this.installCLI(true), 10000 + Math.floor(Math.random() * 20000)); - } + } */ } dispose(): void { From 57bca030019aa91d3a92447ac75c69cff0c71aae Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Wed, 27 Aug 2025 13:11:52 -0700 Subject: [PATCH 14/33] Points release server to supported environment --- src/env/node/gk/cli/integration.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index 987cb859787a7..5b7d7c88f6f66 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -402,14 +402,24 @@ export class GkCliIntegrationProvider implements Disposable { try { // Download the MCP proxy installer - const proxyUrl = this.container.urls.getGkApiUrl( + // TODO: Switch to getGkApiUrl once we support other environments + const proxyUrl = Uri.joinPath( + Uri.parse('https://api.gitkraken.dev'), 'releases', 'gkcli-proxy', 'production', platformName, architecture, 'active', - ); + ).toString(); + /* const proxyUrl = this.container.urls.getGkApiUrl( + 'releases', + 'gkcli-proxy', + 'production', + platformName, + architecture, + 'active', + ); */ let response = await fetch(proxyUrl); if (!response.ok) { From 32ab1efe2f116856c7aefe698e6af4fc2c8ce379 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Thu, 4 Sep 2025 10:18:47 -0700 Subject: [PATCH 15/33] Fixes 'checking for updates' CLI response from causing error with flow --- src/env/node/gk/cli/integration.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index 5b7d7c88f6f66..2864af0946057 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -36,6 +36,12 @@ export interface CliCommandRequest { export type CliCommandResponse = { stdout?: string; stderr?: string } | void; export type CliIpcServer = IpcServer; +const CLIProxyMCPInstallOutputs = { + checkingForUpdates: /checking for updates.../i, + notASupportedClient: /is not a supported MCP client/i, + installedSuccessfully: /GitKraken MCP Server Successfully Installed!/i, +}; + export class GkCliIntegrationProvider implements Disposable { private readonly _disposable: Disposable; private _runningDisposable: Disposable | undefined; @@ -236,8 +242,8 @@ export class GkCliIntegrationProvider implements Disposable { }, ); - output = output.trim(); - if (output === 'GitKraken MCP Server Successfully Installed!') { + output = output.replace(CLIProxyMCPInstallOutputs.checkingForUpdates, '').trim(); + if (CLIProxyMCPInstallOutputs.installedSuccessfully.test(output)) { if (this.container.telemetry.enabled) { this.container.telemetry.sendEvent('mcp/setup/completed', { requiresUserCompletion: false, @@ -246,7 +252,7 @@ export class GkCliIntegrationProvider implements Disposable { }); } return; - } else if (output.includes('not a supported MCP client')) { + } else if (CLIProxyMCPInstallOutputs.notASupportedClient.test(output)) { if (this.container.telemetry.enabled) { this.container.telemetry.sendEvent('mcp/setup/failed', { reason: 'unsupported app', From afc433a163ffbe0f96e6c99f85e7ad01fcee7268 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Thu, 4 Sep 2025 12:12:43 -0700 Subject: [PATCH 16/33] Adds messaging to complete setup manually on unsupported apps --- src/constants.ts | 1 + src/env/node/gk/cli/integration.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/constants.ts b/src/constants.ts index 32a21fe73663c..1a893ee8bb044 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -179,6 +179,7 @@ export const urls = Object.freeze({ githubDiscussions: `https://github.com/gitkraken/vscode-gitlens/discussions/?${utm}`, helpCenter: `https://help.gitkraken.com/gitlens/gitlens-start-here/?${utm}`, helpCenterHome: `https://help.gitkraken.com/gitlens/home-view/?${utm}`, + helpCenterMCP: `https://help.gitkraken.com/mcp/mcp-getting-started/?${utm}`, releaseNotes: `https://help.gitkraken.com/gitlens/gitlens-release-notes-current/?${utm}`, acceleratePrReviews: `https://help.gitkraken.com/gitlens/gitlens-start-here/?${utm}#accelerate-pr-reviews`, diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index 2864af0946057..8a787986eedfa 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -1,6 +1,7 @@ import { arch } from 'process'; import type { ConfigurationChangeEvent } from 'vscode'; import { version as codeVersion, Disposable, env, ProgressLocation, Uri, window, workspace } from 'vscode'; +import { urls } from '../../../../constants'; import type { Source, Sources } from '../../../../constants.telemetry'; import type { Container } from '../../../../container'; import type { SubscriptionChangeEvent } from '../../../../plus/gk/subscriptionService'; @@ -261,6 +262,19 @@ export class GkCliIntegrationProvider implements Disposable { 'cli.version': cliVersion, }); } + + const learnMore = { title: 'Learn More' }; + const cancel = { title: 'Cancel', isCloseAffordance: true }; + const result = await window.showErrorMessage( + 'Automatic setup of the GitKraken MCP server is not supported for this application. To complete setup, you will have to add the GitKraken MCP manually to your MCP configuration.', + { modal: true }, + learnMore, + cancel, + ); + if (result === learnMore) { + void openUrl(urls.helpCenterMCP); + } + return; } From ff9eb4e9c1d94261f8767f38bcc6ad9aa2ac91e7 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Thu, 4 Sep 2025 14:51:00 -0700 Subject: [PATCH 17/33] Updates unsupported app modal and wording --- src/env/node/gk/cli/integration.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index 8a787986eedfa..eaa0db318c6ba 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -263,10 +263,10 @@ export class GkCliIntegrationProvider implements Disposable { }); } - const learnMore = { title: 'Learn More' }; + const learnMore = { title: 'View Setup Instructions' }; const cancel = { title: 'Cancel', isCloseAffordance: true }; - const result = await window.showErrorMessage( - 'Automatic setup of the GitKraken MCP server is not supported for this application. To complete setup, you will have to add the GitKraken MCP manually to your MCP configuration.', + const result = await window.showInformationMessage( + 'This application doesn’t support automatic MCP setup. Please add the GitKraken MCP to your configuration manually.', { modal: true }, learnMore, cancel, From 52c24f023f086638751b30d65d9a603361050648 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Thu, 28 Aug 2025 16:15:07 -0700 Subject: [PATCH 18/33] Creates MCP banner and adds to Home and Graph --- src/constants.commands.ts | 1 + src/constants.storage.ts | 1 + src/env/node/gk/cli/integration.ts | 1 + src/plus/gk/utils/-webview/mcp.utils.ts | 24 +++ src/system/-webview/storage.ts | 25 +++ src/webviews/apps/home/home.ts | 11 ++ src/webviews/apps/home/stateProvider.ts | 8 + src/webviews/apps/plus/graph/graph-app.ts | 6 + src/webviews/apps/plus/graph/stateProvider.ts | 5 + .../shared/components/banner/banner.css.ts | 152 +++++++++++++++++- .../apps/shared/components/banner/banner.ts | 58 ++++++- .../apps/shared/components/mcp-banner.ts | 75 +++++++++ src/webviews/home/homeWebview.ts | 21 +++ src/webviews/home/protocol.ts | 2 + src/webviews/plus/graph/graphWebview.ts | 21 +++ src/webviews/plus/graph/protocol.ts | 3 + 16 files changed, 406 insertions(+), 8 deletions(-) create mode 100644 src/plus/gk/utils/-webview/mcp.utils.ts create mode 100644 src/webviews/apps/shared/components/mcp-banner.ts diff --git a/src/constants.commands.ts b/src/constants.commands.ts index 75960a5a0c15d..df33b4c3bef8e 100644 --- a/src/constants.commands.ts +++ b/src/constants.commands.ts @@ -140,6 +140,7 @@ type InternalGlCommands = | 'gitlens.refreshHover' | 'gitlens.regenerateMarkdownDocument' | 'gitlens.showComposerPage' + | 'gitlens.storage.store' | 'gitlens.toggleFileBlame:codelens' | 'gitlens.toggleFileBlame:mode' | 'gitlens.toggleFileBlame:statusbar' diff --git a/src/constants.storage.ts b/src/constants.storage.ts index 8e06d377cae3a..41f3c15fbccf5 100644 --- a/src/constants.storage.ts +++ b/src/constants.storage.ts @@ -88,6 +88,7 @@ export type GlobalStorage = { 'gk:cli:path': string; 'home:sections:collapsed': string[]; 'home:walkthrough:dismissed': boolean; + 'mcp:banner:dismissed': boolean; 'launchpad:groups:collapsed': StoredLaunchpadGroup[]; 'launchpad:indicator:hasLoaded': boolean; 'launchpad:indicator:hasInteracted': string; diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index eaa0db318c6ba..dac1432cade66 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -99,6 +99,7 @@ export class GkCliIntegrationProvider implements Disposable { @gate() private async setupMCP(source?: Sources): Promise { + await this.container.storage.store('mcp:banner:dismissed', true); const commandSource = source ?? 'commandPalette'; const scope = getLogScope(); let cliVersion: string | undefined; diff --git a/src/plus/gk/utils/-webview/mcp.utils.ts b/src/plus/gk/utils/-webview/mcp.utils.ts new file mode 100644 index 0000000000000..0906e9dc30bdd --- /dev/null +++ b/src/plus/gk/utils/-webview/mcp.utils.ts @@ -0,0 +1,24 @@ +import { getPlatform, isWeb } from '@env/platform'; +import type { Container } from '../../../../container'; +import { getHostAppName } from '../../../../system/-webview/vscode'; + +export async function isMcpBannerEnabled(container: Container): Promise { + // Check if running on web + if (isWeb) { + return false; + } + + // Check platform + const platform = getPlatform(); + if (platform !== 'windows' && platform !== 'macOS' && platform !== 'linux') { + return false; + } + + if (container.storage.get('mcp:banner:dismissed', false)) return false; + + // Check host app + const hostAppName = await getHostAppName(); + const supportedApps = ['code', 'code-insiders', 'cursor', 'windsurf']; + + return hostAppName != null && supportedApps.includes(hostAppName); +} diff --git a/src/system/-webview/storage.ts b/src/system/-webview/storage.ts index 00c4dbfe5a6ae..bdb08f90b3c05 100644 --- a/src/system/-webview/storage.ts +++ b/src/system/-webview/storage.ts @@ -9,10 +9,20 @@ import type { WorkspaceStorage, } from '../../constants.storage'; import { debug } from '../decorators/log'; +import { registerCommand } from './command'; type GlobalStorageKeys = keyof (GlobalStorage & DeprecatedGlobalStorage); type WorkspaceStorageKeys = keyof (WorkspaceStorage & DeprecatedWorkspaceStorage); +const allowedStoreCommandGlobalStorageKeys: GlobalStorageKeys[] = ['mcp:banner:dismissed']; +const allowedStoreCommandWorkspaceStorageKeys: WorkspaceStorageKeys[] = []; + +interface StorageStoreCommandArgs { + key: string; + value: any; + isWorkspace?: boolean; +} + export type StorageChangeEvent = | { /** @@ -46,6 +56,7 @@ export class Storage implements Disposable { this._onDidChange, this._onDidChangeSecrets, this.context.secrets.onDidChange(e => this._onDidChangeSecrets.fire(e)), + registerCommand('gitlens.storage.store', args => this.storeFromCommand(args), this), ); } @@ -179,4 +190,18 @@ export class Storage implements Disposable { await this.context.workspaceState.update(`${extensionPrefix}:${key}`, value); this._onDidChange.fire({ keys: [key], workspace: true }); } + + async storeFromCommand(args: StorageStoreCommandArgs): Promise { + if (args.isWorkspace) { + if (!allowedStoreCommandWorkspaceStorageKeys.includes(args.key as any)) { + return; + } + await this.storeWorkspace(args.key as any, args.value); + } else { + if (!allowedStoreCommandGlobalStorageKeys.includes(args.key as any)) { + return; + } + await this.store(args.key as any, args.value); + } + } } diff --git a/src/webviews/apps/home/home.ts b/src/webviews/apps/home/home.ts index b9866f2d7dac1..cffdd287002ec 100644 --- a/src/webviews/apps/home/home.ts +++ b/src/webviews/apps/home/home.ts @@ -28,6 +28,7 @@ import './components/ai-all-access-banner'; import './components/ama-banner'; import './components/integration-banner'; import './components/preview-banner'; +import '../shared/components/mcp-banner'; import './components/promo-banner'; import './components/repo-alerts'; import '../shared/components/banner/banner'; @@ -88,12 +89,22 @@ export class GlHomeApp extends GlAppHost { () => html` + `, () => html` + `, )} diff --git a/src/webviews/apps/home/stateProvider.ts b/src/webviews/apps/home/stateProvider.ts index a4b46ee6fe545..6bdecb72ef056 100644 --- a/src/webviews/apps/home/stateProvider.ts +++ b/src/webviews/apps/home/stateProvider.ts @@ -3,6 +3,7 @@ import type { State } from '../../home/protocol'; import { DidChangeAiAllAccessBanner, DidChangeIntegrationsConnections, + DidChangeMcpBanner, DidChangeOrgSettings, DidChangePreviewEnabled, DidChangeRepositories, @@ -92,6 +93,13 @@ export class HomeStateProvider implements StateProvider { this._state.aiAllAccessBannerCollapsed = msg.params; this._state.timestamp = Date.now(); + this.provider.setValue(this._state, true); + host.requestUpdate(); + break; + case DidChangeMcpBanner.is(msg): + this._state.mcpBannerCollapsed = msg.params; + this._state.timestamp = Date.now(); + this.provider.setValue(this._state, true); host.requestUpdate(); break; diff --git a/src/webviews/apps/plus/graph/graph-app.ts b/src/webviews/apps/plus/graph/graph-app.ts index 8f8b1f10dcf10..764a051681c4e 100644 --- a/src/webviews/apps/plus/graph/graph-app.ts +++ b/src/webviews/apps/plus/graph/graph-app.ts @@ -24,6 +24,7 @@ import './graph-wrapper/graph-wrapper'; import './hover/graphHover'; import './minimap/minimap-container'; import './sidebar/sidebar'; +import '../../shared/components/mcp-banner'; @customElement('gl-graph-app') export class GraphApp extends SignalWatcher(LitElement) { @@ -67,6 +68,11 @@ export class GraphApp extends SignalWatcher(LitElement) { override render() { return html`
+ , State, AppState this.updateState({ orgSettings: msg.params.orgSettings }); break; + case DidChangeMcpBanner.is(msg): + this.updateState({ mcpBannerCollapsed: msg.params }); + break; + case DidChangeWorkingTreeNotification.is(msg): this.updateState({ workingTreeStats: msg.params.stats }); break; diff --git a/src/webviews/apps/shared/components/banner/banner.css.ts b/src/webviews/apps/shared/components/banner/banner.css.ts index b4468265d4131..c1143cd312606 100644 --- a/src/webviews/apps/shared/components/banner/banner.css.ts +++ b/src/webviews/apps/shared/components/banner/banner.css.ts @@ -30,6 +30,7 @@ export const bannerStyles = css` border-radius: var(--gl-banner-border-radius); position: relative; overflow: hidden; + container-type: inline-size; } /* Solid display mode - same as card background */ @@ -89,6 +90,35 @@ export const bannerStyles = css` ); } + /* Gradient purple display mode - matches the auto-composer container styling */ + .banner--gradient-purple { + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + background: linear-gradient(135deg, #a100ff1a 0%, #255ed11a 100%); + } + + .banner--gradient-purple .banner__title { + font-size: 1.3rem; + color: var(--vscode-foreground); + font-weight: normal; + } + + .banner--gradient-purple .banner__body { + font-size: 1.2rem; + color: var(--vscode-descriptionForeground); + line-height: 1.4; + } + + .banner--gradient-purple .banner__body a { + color: var(--vscode-textLink-foreground); + text-decoration: none; + } + + .banner--gradient-purple .banner__body a:hover { + color: var(--vscode-textLink-activeForeground); + text-decoration: underline; + } + .banner__content { display: flex; flex-direction: column; @@ -97,6 +127,88 @@ export const bannerStyles = css` text-align: center; } + /* Responsive layout */ + .banner--responsive .banner__content { + display: flex; + flex-direction: column; + align-items: stretch; + text-align: left; + gap: var(--gl-banner-gap); + } + + .banner--responsive .banner__text { + display: flex; + flex-direction: column; + gap: 0.4rem; + } + + .banner--responsive .banner__title, + .banner--responsive .banner__body { + text-align: left; + } + + /* < 500px: Stack vertically with full-width buttons */ + .banner--responsive .banner__buttons { + display: flex; + flex-direction: column; + gap: 0.8rem; + margin-top: 0.8rem; + width: 100%; + } + + .banner--responsive .banner__button--primary, + .banner--responsive .banner__button--secondary { + grid-column: unset; + justify-self: unset; + width: 100% !important; + min-width: 100% !important; + max-width: 100% !important; + justify-content: center; + flex: 1; + } + + /* >= 500px: Three-group horizontal layout */ + @container (min-width: 500px) { + .banner--responsive .banner__content { + flex-direction: row; + align-items: center; + gap: 1.6rem; + } + + /* Group 1: Text content (left-aligned) */ + .banner--responsive .banner__text { + flex: 1; + min-width: 0; + align-self: center; + } + + /* Group 2: Buttons (content-sized) */ + .banner--responsive .banner__buttons { + display: flex; + flex-direction: column; + gap: 0.8rem; + margin-top: 0; + width: auto; + flex-shrink: 0; + align-self: center; + } + + .banner--responsive .banner__button--primary, + .banner--responsive .banner__button--secondary { + width: auto; + white-space: nowrap; + } + + /* Group 3: Dismiss button (to the right of buttons) */ + .banner--responsive .banner__dismiss { + position: static !important; + top: auto !important; + right: auto !important; + align-self: center; + flex-shrink: 0; + } + } + .banner__title { font-size: 1.2em; font-weight: bold; @@ -120,7 +232,7 @@ export const bannerStyles = css` width: 100%; } - .banner__button--primary { + .banner:not(.banner--gradient-purple) .banner__button--primary { grid-column: 2; justify-self: center; white-space: nowrap; @@ -131,6 +243,14 @@ export const bannerStyles = css` font-size: 1.2em; } + .banner--gradient-purple .banner__button--primary { + grid-column: 2; + justify-self: center; + white-space: nowrap; + --button-padding: var(--gl-banner-button-padding); + font-size: 1.2em; + } + .banner__button--secondary { grid-column: 3; justify-self: end; @@ -146,6 +266,26 @@ export const bannerStyles = css` justify-self: center; } + /* Dismiss button */ + .banner__dismiss { + position: absolute; + top: 0.8rem; + right: 0.8rem; + --button-background: transparent; + --button-foreground: var(--gl-banner-dim-text-color); + --button-hover-background: color-mix(in lab, var(--gl-banner-dim-text-color) 15%, transparent); + --button-padding: 0.4rem; + z-index: 1; + } + + /* Responsive layout dismiss button */ + .banner--responsive .banner__dismiss { + /* < 500px: Upper right corner (default positioning) */ + position: absolute; + top: 0.8rem; + right: 0.8rem; + } + /* Theme-specific adjustments */ /* Light theme: Brighten gradient colors for better contrast with dark text */ @@ -164,15 +304,17 @@ export const bannerStyles = css` --gl-banner-text-color: #000; } - :host-context(.vscode-dark) .banner__button--primary, - :host-context(.vscode-high-contrast:not(.vscode-high-contrast-light)) .banner__button--primary { + :host-context(.vscode-dark) .banner:not(.banner--gradient-purple) .banner__button--primary, + :host-context(.vscode-high-contrast:not(.vscode-high-contrast-light)) + .banner:not(.banner--gradient-purple) + .banner__button--primary { --button-background: color-mix(in lab, var(--gl-banner-primary-background) 10%, #fff 20%); --button-hover-background: color-mix(in lab, var(--gl-banner-primary-background) 20%, #fff 30%); --button-foreground: #fff; } - :host-context(.vscode-light) .banner__button--primary, - :host-context(.vscode-high-contrast-light) .banner__button--primary { + :host-context(.vscode-light) .banner:not(.banner--gradient-purple) .banner__button--primary, + :host-context(.vscode-high-contrast-light) .banner:not(.banner--gradient-purple) .banner__button--primary { --button-background: color-mix(in lab, var(--gl-banner-primary-background) 8%, #fff 25%); --button-hover-background: color-mix(in lab, var(--gl-banner-primary-background) 15%, #fff 35%); --button-foreground: #000; diff --git a/src/webviews/apps/shared/components/banner/banner.ts b/src/webviews/apps/shared/components/banner/banner.ts index 66bc204398fb5..2c873dfaef426 100644 --- a/src/webviews/apps/shared/components/banner/banner.ts +++ b/src/webviews/apps/shared/components/banner/banner.ts @@ -7,7 +7,7 @@ import '../button'; export const bannerTagName = 'gl-banner'; -export type BannerDisplay = 'solid' | 'outline' | 'gradient' | 'gradient-transparent'; +export type BannerDisplay = 'solid' | 'outline' | 'gradient' | 'gradient-transparent' | 'gradient-purple'; @customElement(bannerTagName) export class GlBanner extends LitElement { @@ -45,10 +45,20 @@ export class GlBanner extends LitElement { @property({ attribute: 'secondary-button-command' }) secondaryButtonCommand?: string; + @property({ type: Boolean, attribute: 'dismissible' }) + dismissible = false; + + @property({ attribute: 'dismiss-href' }) + dismissHref?: string; + + @property({ attribute: 'layout' }) + layout: 'default' | 'responsive' = 'default'; + private get classNames() { return { banner: true, [`banner--${this.display}`]: true, + [`banner--${this.layout}`]: this.layout !== 'default', }; } @@ -59,9 +69,24 @@ export class GlBanner extends LitElement { private renderContent() { return html` + ${this.layout !== 'responsive' && this.dismissible ? this.renderDismissButton() : ''} + `; + } + + private renderDefaultContent() { + return html` + ${this.bannerTitle ? this.renderTitle() : ''} ${this.body ? this.renderBody() : ''} ${this.renderButtons()} + `; + } + + private renderResponsiveContent() { + return html` + + ${this.renderButtons()} ${this.dismissible ? this.renderDismissButton() : ''} `; } @@ -70,7 +95,7 @@ export class GlBanner extends LitElement { } private renderBody() { - return html``; + return html``; } private renderButtons() { @@ -90,6 +115,8 @@ export class GlBanner extends LitElement { return html` + `; + } + private onPrimaryButtonClick(e: Event) { if (this.primaryButtonCommand) { e.preventDefault(); @@ -137,6 +179,16 @@ export class GlBanner extends LitElement { }), ); } + + private onDismissClick(e: Event) { + e.preventDefault(); + this.dispatchEvent( + new CustomEvent('gl-banner-dismiss', { + bubbles: true, + composed: true, + }), + ); + } } declare global { diff --git a/src/webviews/apps/shared/components/mcp-banner.ts b/src/webviews/apps/shared/components/mcp-banner.ts new file mode 100644 index 0000000000000..76f32d77b5812 --- /dev/null +++ b/src/webviews/apps/shared/components/mcp-banner.ts @@ -0,0 +1,75 @@ +import { css, html, LitElement, nothing } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { createCommandLink } from '../../../../system/commands'; +import './banner/banner'; + +export const mcpBannerTagName = 'gl-mcp-banner'; + +export interface McpBannerSource { + source: string; +} + +@customElement(mcpBannerTagName) +export class GlMcpBanner extends LitElement { + static override shadowRootOptions: ShadowRootInit = { + ...LitElement.shadowRootOptions, + delegatesFocus: true, + }; + + static override styles = [ + css` + :host { + display: block; + } + + gl-banner { + margin-bottom: 1.2rem; + } + + :host([layout='responsive']) gl-banner { + margin-bottom: 0; + width: 100%; + } + `, + ]; + + @property() + source: string = 'unknown'; + + @property() + layout: 'default' | 'responsive' = 'default'; + + @state() + private collapsed: boolean = false; + + override render(): unknown { + if (this.collapsed) { + return nothing; + } + + const bodyHtml = + 'Leverage Git and Integration information from GitLens in AI chat. Learn more'; + + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + [mcpBannerTagName]: GlMcpBanner; + } +} diff --git a/src/webviews/home/homeWebview.ts b/src/webviews/home/homeWebview.ts index 2a31955e4e788..6ea92cb5c9646 100644 --- a/src/webviews/home/homeWebview.ts +++ b/src/webviews/home/homeWebview.ts @@ -51,6 +51,7 @@ import type { AIModelChangeEvent } from '../../plus/ai/aiProviderService'; import { showPatchesView } from '../../plus/drafts/actions'; import type { Subscription } from '../../plus/gk/models/subscription'; import type { SubscriptionChangeEvent } from '../../plus/gk/subscriptionService'; +import { isMcpBannerEnabled } from '../../plus/gk/utils/-webview/mcp.utils'; import { isAiAllAccessPromotionActive } from '../../plus/gk/utils/-webview/promo.utils'; import { isSubscriptionTrialOrPaidFromState } from '../../plus/gk/utils/subscription.utils'; import type { ConfiguredIntegrationsChangeEvent } from '../../plus/integrations/authentication/configuredIntegrationService'; @@ -68,6 +69,7 @@ import { } from '../../system/-webview/command'; import { configuration } from '../../system/-webview/configuration'; import { getContext, onDidChangeContext } from '../../system/-webview/context'; +import type { StorageChangeEvent } from '../../system/-webview/storage'; import { openUrl } from '../../system/-webview/vscode/uris'; import { openWorkspace } from '../../system/-webview/vscode/workspaces'; import { debug, log } from '../../system/decorators/log'; @@ -110,6 +112,7 @@ import { DidChangeAiAllAccessBanner, DidChangeIntegrationsConnections, DidChangeLaunchpad, + DidChangeMcpBanner, DidChangeOrgSettings, DidChangeOverviewFilter, DidChangeOverviewRepository, @@ -180,6 +183,7 @@ export class HomeWebviewProvider implements WebviewProvider(scop export const DismissWalkthroughSection = new IpcCommand(scope, 'walkthrough/dismiss'); export const DidChangeAiAllAccessBanner = new IpcNotification(scope, 'ai/allAccess/didChange'); +export const DidChangeMcpBanner = new IpcNotification(scope, 'mcp/didChange'); export const DismissAiAllAccessBannerCommand = new IpcCommand(scope, 'ai/allAccess/dismiss'); export const SetOverviewFilter = new IpcCommand(scope, 'overview/filter/set'); diff --git a/src/webviews/plus/graph/graphWebview.ts b/src/webviews/plus/graph/graphWebview.ts index 50bcca08ec63b..8bb706386af6c 100644 --- a/src/webviews/plus/graph/graphWebview.ts +++ b/src/webviews/plus/graph/graphWebview.ts @@ -106,6 +106,7 @@ import { import { createReference } from '../../../git/utils/reference.utils'; import { isSha, shortenRevision } from '../../../git/utils/revision.utils'; import type { FeaturePreviewChangeEvent, SubscriptionChangeEvent } from '../../../plus/gk/subscriptionService'; +import { isMcpBannerEnabled } from '../../../plus/gk/utils/-webview/mcp.utils'; import type { ConnectionStateChangeEvent } from '../../../plus/integrations/integrationService'; import { getPullRequestBranchDeepLink } from '../../../plus/launchpad/launchpadProvider'; import type { AssociateIssueWithBranchCommandArgs } from '../../../plus/startWork/startWork'; @@ -119,6 +120,7 @@ import { } from '../../../system/-webview/command'; import { configuration } from '../../../system/-webview/configuration'; import { getContext, onDidChangeContext } from '../../../system/-webview/context'; +import type { StorageChangeEvent } from '../../../system/-webview/storage'; import { isDarkTheme, isLightTheme } from '../../../system/-webview/vscode'; import { openUrl } from '../../../system/-webview/vscode/uris'; import type { OpenWorkspaceLocation } from '../../../system/-webview/vscode/workspaces'; @@ -202,6 +204,7 @@ import { DidChangeBranchStateNotification, DidChangeColumnsNotification, DidChangeGraphConfigurationNotification, + DidChangeMcpBanner, DidChangeNotification, DidChangeOrgSettings, DidChangeRefsMetadataNotification, @@ -331,6 +334,7 @@ export class GraphWebviewProvider implements WebviewProvider { @@ -1034,6 +1038,22 @@ export class GraphWebviewProvider implements WebviewProvider(scope, 'avatars/didChange'); +export const DidChangeMcpBanner = new IpcNotification(scope, 'mcp/didChange'); + export interface DidChangeBranchStateParams { branchState: BranchState; } From 57592f0fe7d42faaa28519c6ffa7236746e64897 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Fri, 29 Aug 2025 08:43:36 -0700 Subject: [PATCH 19/33] Adds to visual file history --- .../apps/plus/timeline/stateProvider.ts | 10 +++++++- src/webviews/apps/plus/timeline/timeline.ts | 6 +++++ src/webviews/plus/timeline/protocol.ts | 3 +++ src/webviews/plus/timeline/timelineWebview.ts | 23 +++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/webviews/apps/plus/timeline/stateProvider.ts b/src/webviews/apps/plus/timeline/stateProvider.ts index d58e5addcc977..b8a1387478a98 100644 --- a/src/webviews/apps/plus/timeline/stateProvider.ts +++ b/src/webviews/apps/plus/timeline/stateProvider.ts @@ -1,6 +1,6 @@ import { ContextProvider } from '@lit/context'; import type { State } from '../../../plus/timeline/protocol'; -import { DidChangeNotification } from '../../../plus/timeline/protocol'; +import { DidChangeMcpBanner, DidChangeNotification } from '../../../plus/timeline/protocol'; import type { ReactiveElementHost, StateProvider } from '../../shared/appHost'; import type { Disposable } from '../../shared/events'; import type { HostIpc } from '../../shared/ipc'; @@ -28,6 +28,14 @@ export class TimelineStateProvider implements StateProvider { case DidChangeNotification.is(msg): this._state = { ...msg.params.state, timestamp: Date.now() }; + this.provider.setValue(this._state, true); + host.requestUpdate(); + break; + + case DidChangeMcpBanner.is(msg): + this._state.mcpBannerCollapsed = msg.params; + this._state.timestamp = Date.now(); + this.provider.setValue(this._state, true); host.requestUpdate(); break; diff --git a/src/webviews/apps/plus/timeline/timeline.ts b/src/webviews/apps/plus/timeline/timeline.ts index 8cb1b47ade7c6..b79f7c4535e53 100644 --- a/src/webviews/apps/plus/timeline/timeline.ts +++ b/src/webviews/apps/plus/timeline/timeline.ts @@ -27,6 +27,7 @@ import { timelineBaseStyles, timelineStyles } from './timeline.css'; import './components/chart'; import '../../shared/components/breadcrumbs'; import '../../shared/components/button'; +import '../../shared/components/mcp-banner'; import '../../shared/components/checkbox/checkbox'; import '../../shared/components/code-icon'; import '../../shared/components/copy-container'; @@ -143,6 +144,11 @@ export class GlTimelineApp extends GlAppHost { override render(): unknown { return html`${this.renderGate()}
+
${this.renderBreadcrumbs()} ${this.renderTimeframe()} diff --git a/src/webviews/plus/timeline/protocol.ts b/src/webviews/plus/timeline/protocol.ts index 9e0136a84fcbc..36cfa324d8376 100644 --- a/src/webviews/plus/timeline/protocol.ts +++ b/src/webviews/plus/timeline/protocol.ts @@ -25,6 +25,7 @@ export interface State extends WebviewState { repositories: { count: number; openCount: number }; access: FeatureAccess; + mcpBannerCollapsed?: boolean; } export interface TimelineDatum { @@ -111,3 +112,5 @@ export interface DidChangeParams { state: State; } export const DidChangeNotification = new IpcNotification(scope, 'didChange'); + +export const DidChangeMcpBanner = new IpcNotification(scope, 'mcp/didChange'); diff --git a/src/webviews/plus/timeline/timelineWebview.ts b/src/webviews/plus/timeline/timelineWebview.ts index 745f1f669b69d..4e4c2b85d7874 100644 --- a/src/webviews/plus/timeline/timelineWebview.ts +++ b/src/webviews/plus/timeline/timelineWebview.ts @@ -34,6 +34,7 @@ import { shortenRevision, } from '../../../git/utils/revision.utils'; import type { SubscriptionChangeEvent } from '../../../plus/gk/subscriptionService'; +import { isMcpBannerEnabled } from '../../../plus/gk/utils/-webview/mcp.utils'; import { Directive } from '../../../quickpicks/items/directive'; import { ReferencesQuickPickIncludes, showReferencePicker2 } from '../../../quickpicks/referencePicker'; import { getRepositoryPickerTitleAndPlaceholder, showRepositoryPicker2 } from '../../../quickpicks/repositoryPicker'; @@ -41,6 +42,7 @@ import { showRevisionFilesPicker } from '../../../quickpicks/revisionFilesPicker import { executeCommand, registerCommand } from '../../../system/-webview/command'; import { configuration } from '../../../system/-webview/configuration'; import { isDescendant } from '../../../system/-webview/path'; +import type { StorageChangeEvent } from '../../../system/-webview/storage'; import { openTextEditor } from '../../../system/-webview/vscode/editors'; import { getTabUri } from '../../../system/-webview/vscode/tabs'; import { createFromDateDelta } from '../../../system/date'; @@ -76,6 +78,7 @@ import type { import { ChoosePathRequest, ChooseRefRequest, + DidChangeMcpBanner, DidChangeNotification, SelectDataPointCommand, UpdateConfigCommand, @@ -283,10 +286,12 @@ export class TimelineWebviewProvider implements WebviewProvider { const dateFormat = configuration.get('defaultDateFormat') ?? 'MMMM Do, YYYY h:mma'; @@ -678,6 +699,7 @@ export class TimelineWebviewProvider implements WebviewProvider Date: Thu, 4 Sep 2025 11:16:52 -0400 Subject: [PATCH 20/33] Updates MCP banner placements --- src/webviews/apps/plus/graph/graph-app.ts | 5 -- src/webviews/apps/plus/graph/graph-header.ts | 30 +++++++++++- .../apps/plus/timeline/stateProvider.ts | 10 +--- src/webviews/apps/plus/timeline/timeline.ts | 6 --- .../shared/components/banner/banner.css.ts | 6 +-- .../apps/shared/components/banner/banner.ts | 47 +++++++++---------- .../apps/shared/components/mcp-banner.ts | 1 + src/webviews/plus/timeline/protocol.ts | 3 -- src/webviews/plus/timeline/timelineWebview.ts | 23 --------- 9 files changed, 56 insertions(+), 75 deletions(-) diff --git a/src/webviews/apps/plus/graph/graph-app.ts b/src/webviews/apps/plus/graph/graph-app.ts index 764a051681c4e..bbdbdc2f853d2 100644 --- a/src/webviews/apps/plus/graph/graph-app.ts +++ b/src/webviews/apps/plus/graph/graph-app.ts @@ -68,11 +68,6 @@ export class GraphApp extends SignalWatcher(LitElement) { override render() { return html`
-
+ ${when( + !(this.graphState.state.mcpBannerCollapsed ?? true), + () => html` + + + + + + Install GitKraken MCP for GitLens
+ Leverage Git and Integration information from GitLens in AI chat. + Learn more +
+
+ `, + )} { case DidChangeNotification.is(msg): this._state = { ...msg.params.state, timestamp: Date.now() }; - this.provider.setValue(this._state, true); - host.requestUpdate(); - break; - - case DidChangeMcpBanner.is(msg): - this._state.mcpBannerCollapsed = msg.params; - this._state.timestamp = Date.now(); - this.provider.setValue(this._state, true); host.requestUpdate(); break; diff --git a/src/webviews/apps/plus/timeline/timeline.ts b/src/webviews/apps/plus/timeline/timeline.ts index b79f7c4535e53..8cb1b47ade7c6 100644 --- a/src/webviews/apps/plus/timeline/timeline.ts +++ b/src/webviews/apps/plus/timeline/timeline.ts @@ -27,7 +27,6 @@ import { timelineBaseStyles, timelineStyles } from './timeline.css'; import './components/chart'; import '../../shared/components/breadcrumbs'; import '../../shared/components/button'; -import '../../shared/components/mcp-banner'; import '../../shared/components/checkbox/checkbox'; import '../../shared/components/code-icon'; import '../../shared/components/copy-container'; @@ -144,11 +143,6 @@ export class GlTimelineApp extends GlAppHost { override render(): unknown { return html`${this.renderGate()}
-
${this.renderBreadcrumbs()} ${this.renderTimeframe()} diff --git a/src/webviews/apps/shared/components/banner/banner.css.ts b/src/webviews/apps/shared/components/banner/banner.css.ts index c1143cd312606..8894e8d5e2720 100644 --- a/src/webviews/apps/shared/components/banner/banner.css.ts +++ b/src/webviews/apps/shared/components/banner/banner.css.ts @@ -156,8 +156,7 @@ export const bannerStyles = css` width: 100%; } - .banner--responsive .banner__button--primary, - .banner--responsive .banner__button--secondary { + .banner--responsive .banner__button { grid-column: unset; justify-self: unset; width: 100% !important; @@ -193,8 +192,7 @@ export const bannerStyles = css` align-self: center; } - .banner--responsive .banner__button--primary, - .banner--responsive .banner__button--secondary { + .banner--responsive .banner__button { width: auto; white-space: nowrap; } diff --git a/src/webviews/apps/shared/components/banner/banner.ts b/src/webviews/apps/shared/components/banner/banner.ts index 2c873dfaef426..e1aeb8fa5c3ac 100644 --- a/src/webviews/apps/shared/components/banner/banner.ts +++ b/src/webviews/apps/shared/components/banner/banner.ts @@ -2,6 +2,7 @@ import { html, LitElement } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { ifDefined } from 'lit/directives/if-defined.js'; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { bannerStyles } from './banner.css'; import '../button'; @@ -62,56 +63,50 @@ export class GlBanner extends LitElement { }; } - override render(): unknown { - return html`
${this.renderContent()}
`; - } - - private renderContent() { - return html` + override render() { + return html`
- ${this.layout !== 'responsive' && this.dismissible ? this.renderDismissButton() : ''} - `; + ${this.layout !== 'responsive' ? this.renderDismissButton() : undefined} +
`; } private renderDefaultContent() { - return html` - ${this.bannerTitle ? this.renderTitle() : ''} ${this.body ? this.renderBody() : ''} ${this.renderButtons()} - `; + return html`${this.renderTitle()} ${this.renderBody()} ${this.renderButtons()}`; } private renderResponsiveContent() { return html` - - ${this.renderButtons()} ${this.dismissible ? this.renderDismissButton() : ''} + + ${this.renderButtons()} ${this.renderDismissButton()} `; } private renderTitle() { + if (!this.bannerTitle) return undefined; + return html``; } private renderBody() { - return html``; + if (!this.body) return undefined; + + return html``; } private renderButtons() { - const hasPrimary = this.primaryButton; - const hasSecondary = this.secondaryButton; + const primary = this.renderPrimaryButton(); + const secondary = this.renderSecondaryButton(); - if (!hasPrimary && !hasSecondary) return ''; + if (!primary && !secondary) return undefined; - return html` - - `; + return html``; } private renderPrimaryButton() { + if (!this.primaryButton) return undefined; + return html`
Learn more'; + const bodyHtml = `Leverage Git and Integration information from GitLens in AI chat. Learn more`; return html` Date: Thu, 4 Sep 2025 16:42:42 -0400 Subject: [PATCH 22/33] Adds registerMcpServerDefinitionProvider (wip) --- package.json | 6 ++ src/@types/vscode.lm.mcp.d.ts | 184 ++++++++++++++++++++++++++++++++++ src/container.ts | 6 ++ src/env/node/gk/mcp.ts | 47 +++++++++ src/env/node/providers.ts | 5 + 5 files changed, 248 insertions(+) create mode 100644 src/@types/vscode.lm.mcp.d.ts create mode 100644 src/env/node/gk/mcp.ts diff --git a/package.json b/package.json index 99085f90823d7..811a50b5e4126 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,12 @@ } }, "contributes": { + "mcpServerDefinitionProviders": [ + { + "id": "gitlens.mcpProvider", + "label": "GitLens MCP Provider" + } + ], "configuration": [ { "id": "current-line-blame", diff --git a/src/@types/vscode.lm.mcp.d.ts b/src/@types/vscode.lm.mcp.d.ts new file mode 100644 index 0000000000000..9a69f5f389277 --- /dev/null +++ b/src/@types/vscode.lm.mcp.d.ts @@ -0,0 +1,184 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + * See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Type Definition for Visual Studio Code 1.101 Extension API + * See https://code.visualstudio.com/api for more information + */ + +declare module 'vscode' { + /** + * McpStdioServerDefinition represents an MCP server available by running + * a local process and operating on its stdin and stdout streams. The process + * will be spawned as a child process of the extension host and by default + * will not run in a shell environment. + */ + export class McpStdioServerDefinition { + /** + * The human-readable name of the server. + */ + readonly label: string; + + /** + * The working directory used to start the server. + */ + cwd?: Uri; + + /** + * The command used to start the server. Node.js-based servers may use + * `process.execPath` to use the editor's version of Node.js to run the script. + */ + command: string; + + /** + * Additional command-line arguments passed to the server. + */ + args: string[]; + + /** + * Optional additional environment information for the server. Variables + * in this environment will overwrite or remove (if null) the default + * environment variables of the editor's extension host. + */ + env: Record; + + /** + * Optional version identification for the server. If this changes, the + * editor will indicate that tools have changed and prompt to refresh them. + */ + version?: string; + + /** + * @param label The human-readable name of the server. + * @param command The command used to start the server. + * @param args Additional command-line arguments passed to the server. + * @param env Optional additional environment information for the server. + * @param version Optional version identification for the server. + */ + constructor( + label: string, + command: string, + args?: string[], + env?: Record, + version?: string, + ); + } + + /** + * McpHttpServerDefinition represents an MCP server available using the + * Streamable HTTP transport. + */ + export class McpHttpServerDefinition { + /** + * The human-readable name of the server. + */ + readonly label: string; + + /** + * The URI of the server. The editor will make a POST request to this URI + * to begin each session. + */ + uri: Uri; + + /** + * Optional additional heads included with each request to the server. + */ + headers: Record; + + /** + * Optional version identification for the server. If this changes, the + * editor will indicate that tools have changed and prompt to refresh them. + */ + version?: string; + + /** + * @param label The human-readable name of the server. + * @param uri The URI of the server. + * @param headers Optional additional heads included with each request to the server. + */ + constructor(label: string, uri: Uri, headers?: Record, version?: string); + } + + /** + * Definitions that describe different types of Model Context Protocol servers, + * which can be returned from the {@link McpServerDefinitionProvider}. + */ + export type McpServerDefinition = McpStdioServerDefinition | McpHttpServerDefinition; + + /** + * A type that can provide Model Context Protocol server definitions. This + * should be registered using {@link lm.registerMcpServerDefinitionProvider} + * during extension activation. + */ + export interface McpServerDefinitionProvider { + /** + * Optional event fired to signal that the set of available servers has changed. + */ + readonly onDidChangeMcpServerDefinitions?: Event; + + /** + * Provides available MCP servers. The editor will call this method eagerly + * to ensure the availability of servers for the language model, and so + * extensions should not take actions which would require user + * interaction, such as authentication. + * + * @param token A cancellation token. + * @returns An array of MCP available MCP servers + */ + provideMcpServerDefinitions(token: CancellationToken): ProviderResult; + + /** + * This function will be called when the editor needs to start a MCP server. + * At this point, the extension may take any actions which may require user + * interaction, such as authentication. Any non-`readonly` property of the + * server may be modified, and the extension should return the resolved server. + * + * The extension may return undefined to indicate that the server + * should not be started, or throw an error. If there is a pending tool + * call, the editor will cancel it and return an error message to the + * language model. + * + * @param server The MCP server to resolve + * @param token A cancellation token. + * @returns The resolved server or thenable that resolves to such. This may + * be the given `server` definition with non-readonly properties filled in. + */ + resolveMcpServerDefinition?(server: T, token: CancellationToken): ProviderResult; + } + + export namespace lm { + /** + * Registers a provider that publishes Model Context Protocol servers for the editor to + * consume. This allows MCP servers to be dynamically provided to the editor in + * addition to those the user creates in their configuration files. + * + * Before calling this method, extensions must register the `contributes.mcpServerDefinitionProviders` + * extension point with the corresponding {@link id}, for example: + * + * ```js + * "contributes": { + * "mcpServerDefinitionProviders": [ + * { + * "id": "cool-cloud-registry.mcp-servers", + * "label": "Cool Cloud Registry", + * } + * ] + * } + * ``` + * + * When a new McpServerDefinitionProvider is available, the editor will present a 'refresh' + * action to the user to discover new servers. To enable this flow, extensions should + * call `registerMcpServerDefinitionProvider` during activation. + * @param id The ID of the provider, which is unique to the extension. + * @param provider The provider to register + * @returns A disposable that unregisters the provider when disposed. + */ + export function registerMcpServerDefinitionProvider( + id: string, + provider: McpServerDefinitionProvider, + ): Disposable; + } +} diff --git a/src/container.ts b/src/container.ts index 3b64e7905c390..0f761c5105f38 100644 --- a/src/container.ts +++ b/src/container.ts @@ -2,6 +2,7 @@ import type { ConfigurationChangeEvent, Disposable, Event, ExtensionContext } fr import { EventEmitter, ExtensionMode } from 'vscode'; import { getGkCliIntegrationProvider, + getMcpProvider, getSharedGKStorageLocationProvider, getSupportedGitProviders, getSupportedRepositoryLocationProvider, @@ -230,6 +231,11 @@ export class Container { this._disposables.push((this._statusBarController = new StatusBarController(this))); this._disposables.push((this._codeLensController = new GitCodeLensController(this))); + const mcpProvider = getMcpProvider(this); + if (mcpProvider != null) { + this._disposables.push(mcpProvider); + } + const webviews = new WebviewsController(this); this._disposables.push(webviews); this._disposables.push((this._views = new Views(this, webviews))); diff --git a/src/env/node/gk/mcp.ts b/src/env/node/gk/mcp.ts new file mode 100644 index 0000000000000..0609b4316a8fa --- /dev/null +++ b/src/env/node/gk/mcp.ts @@ -0,0 +1,47 @@ +import type { Event, McpServerDefinition, McpStdioServerDefinition } from 'vscode'; +import { version as codeVersion, Disposable, EventEmitter, lm } from 'vscode'; +import type { Container } from '../../../container'; +import { satisfies } from '../../../system/version'; + +export class McpProvider implements Disposable { + static #instance: McpProvider | undefined; + + static create(container: Container): McpProvider | undefined { + if (!satisfies(codeVersion, '>= 1.101.0') || !lm.registerMcpServerDefinitionProvider) return undefined; + + if (this.#instance == null) { + this.#instance = new McpProvider(container); + } + + return this.#instance; + } + + private readonly _disposable: Disposable; + private readonly _onDidChangeMcpServerDefinitions = new EventEmitter(); + get onDidChangeMcpServerDefinitions(): Event { + return this._onDidChangeMcpServerDefinitions.event; + } + + private serverDefinitions: McpServerDefinition[] = []; + + private constructor(private readonly container: Container) { + this._disposable = Disposable.from( + lm.registerMcpServerDefinitionProvider('gitlens.mcpProvider', { + onDidChangeMcpServerDefinitions: this._onDidChangeMcpServerDefinitions.event, + provideMcpServerDefinitions: () => this.provideMcpServerDefinitions(), + }), + ); + } + + private provideMcpServerDefinitions(): Promise { + return Promise.resolve([]); + } + + registerMcpServer(): Promise { + return Promise.resolve(); + } + + dispose(): void { + this._disposable.dispose(); + } +} diff --git a/src/env/node/providers.ts b/src/env/node/providers.ts index 1b70763b14c84..2bc8432561639 100644 --- a/src/env/node/providers.ts +++ b/src/env/node/providers.ts @@ -14,6 +14,7 @@ import { GkCliIntegrationProvider } from './gk/cli/integration'; import { LocalRepositoryLocationProvider } from './gk/localRepositoryLocationProvider'; import { LocalSharedGkStorageLocationProvider } from './gk/localSharedGkStorageLocationProvider'; import { LocalGkWorkspacesSharedStorageProvider } from './gk/localWorkspacesSharedStorageProvider'; +import { McpProvider } from './gk/mcp'; let gitInstance: Git | undefined; function ensureGit(container: Container) { @@ -71,3 +72,7 @@ export function getSupportedWorkspacesStorageProvider( export function getGkCliIntegrationProvider(container: Container): GkCliIntegrationProvider { return new GkCliIntegrationProvider(container); } + +export function getMcpProvider(container: Container): McpProvider | undefined { + return McpProvider.create(container); +} From 6ef81289346acebe3a649e8f82ad856c050701e5 Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Fri, 5 Sep 2025 17:48:06 -0400 Subject: [PATCH 23/33] Adds provideMcpServerDefinitions --- src/container.ts | 10 +-- src/env/browser/providers.ts | 4 ++ src/env/node/gk/cli/integration.ts | 14 +--- src/env/node/gk/mcp.ts | 47 ------------ src/env/node/gk/mcp/integration.ts | 112 +++++++++++++++++++++++++++++ src/env/node/gk/mcp/utils.ts | 12 ++++ src/env/node/providers.ts | 2 +- 7 files changed, 135 insertions(+), 66 deletions(-) delete mode 100644 src/env/node/gk/mcp.ts create mode 100644 src/env/node/gk/mcp/integration.ts create mode 100644 src/env/node/gk/mcp/utils.ts diff --git a/src/container.ts b/src/container.ts index 0f761c5105f38..b3f458b8eefc4 100644 --- a/src/container.ts +++ b/src/container.ts @@ -231,11 +231,6 @@ export class Container { this._disposables.push((this._statusBarController = new StatusBarController(this))); this._disposables.push((this._codeLensController = new GitCodeLensController(this))); - const mcpProvider = getMcpProvider(this); - if (mcpProvider != null) { - this._disposables.push(mcpProvider); - } - const webviews = new WebviewsController(this); this._disposables.push(webviews); this._disposables.push((this._views = new Views(this, webviews))); @@ -277,6 +272,11 @@ export class Container { this._disposables.push(cliIntegration); } + const mcpProvider = getMcpProvider(this); + if (mcpProvider != null) { + this._disposables.push(mcpProvider); + } + this._disposables.push( configuration.onDidChange(e => { if (configuration.changed(e, 'terminalLinks.enabled')) { diff --git a/src/env/browser/providers.ts b/src/env/browser/providers.ts index 07ac33409f79e..d5403929a789d 100644 --- a/src/env/browser/providers.ts +++ b/src/env/browser/providers.ts @@ -42,3 +42,7 @@ export function getSupportedWorkspacesStorageProvider( export function getGkCliIntegrationProvider(_container: Container): undefined { return undefined; } + +export function getMcpProvider(_container: Container): undefined { + return undefined; +} diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index dac1432cade66..056f151ae2f54 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -15,6 +15,7 @@ import { getLogScope } from '../../../../system/logger.scope'; import { compare } from '../../../../system/version'; import { run } from '../../git/shell'; import { getPlatform, isWeb } from '../../platform'; +import { toMcpInstallProvider } from '../mcp/utils'; import { CliCommandHandlers } from './commands'; import type { IpcServer } from './ipcServer'; import { createIpcServer } from './ipcServer'; @@ -731,16 +732,3 @@ class CLIInstallError extends Error { return message; } } - -function toMcpInstallProvider(appHostName: string | undefined): string | undefined { - switch (appHostName) { - case 'code': - return 'vscode'; - case 'code-insiders': - return 'vscode-insiders'; - case 'code-exploration': - return 'vscode-exploration'; - default: - return appHostName; - } -} diff --git a/src/env/node/gk/mcp.ts b/src/env/node/gk/mcp.ts deleted file mode 100644 index 0609b4316a8fa..0000000000000 --- a/src/env/node/gk/mcp.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { Event, McpServerDefinition, McpStdioServerDefinition } from 'vscode'; -import { version as codeVersion, Disposable, EventEmitter, lm } from 'vscode'; -import type { Container } from '../../../container'; -import { satisfies } from '../../../system/version'; - -export class McpProvider implements Disposable { - static #instance: McpProvider | undefined; - - static create(container: Container): McpProvider | undefined { - if (!satisfies(codeVersion, '>= 1.101.0') || !lm.registerMcpServerDefinitionProvider) return undefined; - - if (this.#instance == null) { - this.#instance = new McpProvider(container); - } - - return this.#instance; - } - - private readonly _disposable: Disposable; - private readonly _onDidChangeMcpServerDefinitions = new EventEmitter(); - get onDidChangeMcpServerDefinitions(): Event { - return this._onDidChangeMcpServerDefinitions.event; - } - - private serverDefinitions: McpServerDefinition[] = []; - - private constructor(private readonly container: Container) { - this._disposable = Disposable.from( - lm.registerMcpServerDefinitionProvider('gitlens.mcpProvider', { - onDidChangeMcpServerDefinitions: this._onDidChangeMcpServerDefinitions.event, - provideMcpServerDefinitions: () => this.provideMcpServerDefinitions(), - }), - ); - } - - private provideMcpServerDefinitions(): Promise { - return Promise.resolve([]); - } - - registerMcpServer(): Promise { - return Promise.resolve(); - } - - dispose(): void { - this._disposable.dispose(); - } -} diff --git a/src/env/node/gk/mcp/integration.ts b/src/env/node/gk/mcp/integration.ts new file mode 100644 index 0000000000000..84fcb9e5ff7ec --- /dev/null +++ b/src/env/node/gk/mcp/integration.ts @@ -0,0 +1,112 @@ +import type { Event, McpServerDefinition } from 'vscode'; +import { + version as codeVersion, + Disposable, + env, + EventEmitter, + lm, + McpStdioServerDefinition, + Uri, + window, +} from 'vscode'; +import type { Container } from '../../../../container'; +import type { StorageChangeEvent } from '../../../../system/-webview/storage'; +import { getHostAppName } from '../../../../system/-webview/vscode'; +import { debounce } from '../../../../system/function/debounce'; +import { satisfies } from '../../../../system/version'; +import { getPlatform } from '../../platform'; +import { toMcpInstallProvider } from './utils'; + +export class McpProvider implements Disposable { + static #instance: McpProvider | undefined; + + static create(container: Container): McpProvider | undefined { + if (!satisfies(codeVersion, '>= 1.101.0') || !lm.registerMcpServerDefinitionProvider) return undefined; + + if (this.#instance == null) { + this.#instance = new McpProvider(container); + } + + return this.#instance; + } + + private readonly _disposable: Disposable; + private readonly _onDidChangeMcpServerDefinitions = new EventEmitter(); + get onDidChangeMcpServerDefinitions(): Event { + return this._onDidChangeMcpServerDefinitions.event; + } + + private constructor(private readonly container: Container) { + this._disposable = Disposable.from( + this.container.storage.onDidChange(e => this.checkStorage(e)), + lm.registerMcpServerDefinitionProvider('gitlens.mcpProvider', { + onDidChangeMcpServerDefinitions: this._onDidChangeMcpServerDefinitions.event, + provideMcpServerDefinitions: () => this.provideMcpServerDefinitions(), + }), + ); + + this.checkStorage(); + } + + private checkStorage(e?: StorageChangeEvent): void { + if (e != null && !(e.keys as string[]).includes('gk:cli:install')) return; + this._onDidChangeMcpServerDefinitions.fire(); + } + + private async provideMcpServerDefinitions(): Promise { + const config = await this.getMcpConfiguration(); + if (config == null) { + return []; + } + + const serverDefinition = new McpStdioServerDefinition( + config.name, + config.command, + config.args, + {}, + config.version, + ); + + this.notifyServerProvided(); + + return [serverDefinition]; + } + + private async getMcpConfiguration(): Promise< + { name: string; type: string; command: string; args: string[]; version?: string } | undefined + > { + const cliInstall = this.container.storage.get('gk:cli:install'); + const cliPath = this.container.storage.get('gk:cli:path'); + + if (cliInstall?.status !== 'completed' || !cliPath) { + return undefined; + } + + const platform = getPlatform(); + const executable = platform === 'windows' ? 'gk.exe' : 'gk'; + const command = Uri.joinPath(Uri.file(cliPath), executable); + + const appName = toMcpInstallProvider(await getHostAppName()); + const args = ['mcp', `--host=${appName}`, '--source=gitlens', `--scheme=${env.uriScheme}`]; + return { + name: 'GitKraken MCP Server', + type: 'stdio', + command: command.fsPath, + args: args, + version: cliInstall.version, + }; + } + + dispose(): void { + this._disposable.dispose(); + this._onDidChangeMcpServerDefinitions.dispose(); + } + + private _notifyServerProvided = false; + private notifyServerProvided = debounce(() => { + if (this._notifyServerProvided) return; + + void window.showInformationMessage('GitLens can now automatically configure the GitKraken MCP server for you'); + this._notifyServerProvided = true; + }, 250); +} diff --git a/src/env/node/gk/mcp/utils.ts b/src/env/node/gk/mcp/utils.ts new file mode 100644 index 0000000000000..01917816831f3 --- /dev/null +++ b/src/env/node/gk/mcp/utils.ts @@ -0,0 +1,12 @@ +export function toMcpInstallProvider(appHostName: string | undefined): string | undefined { + switch (appHostName) { + case 'code': + return 'vscode'; + case 'code-insiders': + return 'vscode-insiders'; + case 'code-exploration': + return 'vscode-exploration'; + default: + return appHostName; + } +} diff --git a/src/env/node/providers.ts b/src/env/node/providers.ts index 2bc8432561639..cac84be056cc8 100644 --- a/src/env/node/providers.ts +++ b/src/env/node/providers.ts @@ -14,7 +14,7 @@ import { GkCliIntegrationProvider } from './gk/cli/integration'; import { LocalRepositoryLocationProvider } from './gk/localRepositoryLocationProvider'; import { LocalSharedGkStorageLocationProvider } from './gk/localSharedGkStorageLocationProvider'; import { LocalGkWorkspacesSharedStorageProvider } from './gk/localWorkspacesSharedStorageProvider'; -import { McpProvider } from './gk/mcp'; +import { McpProvider } from './gk/mcp/integration'; let gitInstance: Git | undefined; function ensureGit(container: Container) { From 421cb8473017ade96d92012414a15bc5ffcaf9a8 Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Fri, 5 Sep 2025 18:08:42 -0400 Subject: [PATCH 24/33] Update provideMcpServerDefinitions to get MCP config from CLI --- src/env/node/gk/mcp/integration.ts | 99 +++++++++++++++++++++++------- 1 file changed, 76 insertions(+), 23 deletions(-) diff --git a/src/env/node/gk/mcp/integration.ts b/src/env/node/gk/mcp/integration.ts index 84fcb9e5ff7ec..6cd5901c2446d 100644 --- a/src/env/node/gk/mcp/integration.ts +++ b/src/env/node/gk/mcp/integration.ts @@ -1,22 +1,19 @@ import type { Event, McpServerDefinition } from 'vscode'; -import { - version as codeVersion, - Disposable, - env, - EventEmitter, - lm, - McpStdioServerDefinition, - Uri, - window, -} from 'vscode'; +import { version as codeVersion, Disposable, env, EventEmitter, lm, McpStdioServerDefinition, window } from 'vscode'; import type { Container } from '../../../../container'; import type { StorageChangeEvent } from '../../../../system/-webview/storage'; import { getHostAppName } from '../../../../system/-webview/vscode'; import { debounce } from '../../../../system/function/debounce'; +import { Logger } from '../../../../system/logger'; import { satisfies } from '../../../../system/version'; +import { run } from '../../git/shell'; import { getPlatform } from '../../platform'; import { toMcpInstallProvider } from './utils'; +const CLIProxyMCPConfigOutputs = { + checkingForUpdates: /checking for updates.../i, +}; + export class McpProvider implements Disposable { static #instance: McpProvider | undefined; @@ -54,7 +51,7 @@ export class McpProvider implements Disposable { } private async provideMcpServerDefinitions(): Promise { - const config = await this.getMcpConfiguration(); + const config = await this.getMcpConfigurationFromCLI(); if (config == null) { return []; } @@ -72,7 +69,32 @@ export class McpProvider implements Disposable { return [serverDefinition]; } - private async getMcpConfiguration(): Promise< + // private async getMcpConfiguration(): Promise< + // { name: string; type: string; command: string; args: string[]; version?: string } | undefined + // > { + // const cliInstall = this.container.storage.get('gk:cli:install'); + // const cliPath = this.container.storage.get('gk:cli:path'); + + // if (cliInstall?.status !== 'completed' || !cliPath) { + // return undefined; + // } + + // const platform = getPlatform(); + // const executable = platform === 'windows' ? 'gk.exe' : 'gk'; + // const command = Uri.joinPath(Uri.file(cliPath), executable); + + // const appName = toMcpInstallProvider(await getHostAppName()); + // const args = ['mcp', `--host=${appName}`, '--source=gitlens', `--scheme=${env.uriScheme}`]; + // return { + // name: 'GitKraken MCP Server', + // type: 'stdio', + // command: command.fsPath, + // args: args, + // version: cliInstall.version, + // }; + // } + + private async getMcpConfigurationFromCLI(): Promise< { name: string; type: string; command: string; args: string[]; version?: string } | undefined > { const cliInstall = this.container.storage.get('gk:cli:install'); @@ -82,19 +104,50 @@ export class McpProvider implements Disposable { return undefined; } + const appName = toMcpInstallProvider(await getHostAppName()); + if (appName == null) { + return undefined; + } + + let output = await this.runCLICommand( + ['mcp', 'config', appName, '--source=gitlens', `--scheme=${env.uriScheme}`], + { + cwd: cliPath, + }, + ); + output = output.replace(CLIProxyMCPConfigOutputs.checkingForUpdates, '').trim(); + console.log(output); + + try { + const configuration = JSON.parse(output) as { name: string; type: string; command: string; args: string[] }; + + return { + name: configuration.name, + type: configuration.type, + command: configuration.command, + args: configuration.args, + version: cliInstall.version, + }; + } catch (ex) { + Logger.error(`Error getting MCP configuration: ${ex}`); + } + + return undefined; + } + + private async runCLICommand( + args: string[], + options?: { + cwd?: string; + }, + ): Promise { const platform = getPlatform(); - const executable = platform === 'windows' ? 'gk.exe' : 'gk'; - const command = Uri.joinPath(Uri.file(cliPath), executable); + const cwd = options?.cwd ?? this.container.storage.get('gk:cli:path'); + if (cwd == null) { + throw new Error('CLI is not installed'); + } - const appName = toMcpInstallProvider(await getHostAppName()); - const args = ['mcp', `--host=${appName}`, '--source=gitlens', `--scheme=${env.uriScheme}`]; - return { - name: 'GitKraken MCP Server', - type: 'stdio', - command: command.fsPath, - args: args, - version: cliInstall.version, - }; + return run(platform === 'windows' ? 'gk.exe' : './gk', args, 'utf8', { cwd: cwd }); } dispose(): void { From 78292e98379ef9f68c376cbddf02181eee604eaf Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Fri, 5 Sep 2025 18:44:57 -0400 Subject: [PATCH 25/33] Prevents older ide versions throwing errors due to McpProvider --- src/container.ts | 14 +++++++++----- src/env/node/gk/mcp/integration.ts | 17 ++--------------- src/env/node/providers.ts | 12 +++++++++--- src/plus/gk/utils/-webview/mcp.utils.ts | 6 ++++++ 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/container.ts b/src/container.ts index b3f458b8eefc4..131d9d095813f 100644 --- a/src/container.ts +++ b/src/container.ts @@ -272,11 +272,6 @@ export class Container { this._disposables.push(cliIntegration); } - const mcpProvider = getMcpProvider(this); - if (mcpProvider != null) { - this._disposables.push(mcpProvider); - } - this._disposables.push( configuration.onDidChange(e => { if (configuration.changed(e, 'terminalLinks.enabled')) { @@ -325,6 +320,7 @@ export class Container { this._ready = true; await this.registerGitProviders(); + await this.registerMcpProvider(); queueMicrotask(() => this._onReady.fire()); } @@ -339,6 +335,14 @@ export class Container { void this._git.registrationComplete(); } + @log() + private async registerMcpProvider(): Promise { + const mcpProvider = await getMcpProvider(this); + if (mcpProvider != null) { + this._disposables.push(mcpProvider); + } + } + private onAnyConfigurationChanged(e: ConfigurationChangeEvent) { if (!configuration.changedAny(e, extensionPrefix)) return; diff --git a/src/env/node/gk/mcp/integration.ts b/src/env/node/gk/mcp/integration.ts index 6cd5901c2446d..ca42ba39ffc80 100644 --- a/src/env/node/gk/mcp/integration.ts +++ b/src/env/node/gk/mcp/integration.ts @@ -1,11 +1,10 @@ import type { Event, McpServerDefinition } from 'vscode'; -import { version as codeVersion, Disposable, env, EventEmitter, lm, McpStdioServerDefinition, window } from 'vscode'; +import { Disposable, env, EventEmitter, lm, McpStdioServerDefinition, window } from 'vscode'; import type { Container } from '../../../../container'; import type { StorageChangeEvent } from '../../../../system/-webview/storage'; import { getHostAppName } from '../../../../system/-webview/vscode'; import { debounce } from '../../../../system/function/debounce'; import { Logger } from '../../../../system/logger'; -import { satisfies } from '../../../../system/version'; import { run } from '../../git/shell'; import { getPlatform } from '../../platform'; import { toMcpInstallProvider } from './utils'; @@ -15,25 +14,13 @@ const CLIProxyMCPConfigOutputs = { }; export class McpProvider implements Disposable { - static #instance: McpProvider | undefined; - - static create(container: Container): McpProvider | undefined { - if (!satisfies(codeVersion, '>= 1.101.0') || !lm.registerMcpServerDefinitionProvider) return undefined; - - if (this.#instance == null) { - this.#instance = new McpProvider(container); - } - - return this.#instance; - } - private readonly _disposable: Disposable; private readonly _onDidChangeMcpServerDefinitions = new EventEmitter(); get onDidChangeMcpServerDefinitions(): Event { return this._onDidChangeMcpServerDefinitions.event; } - private constructor(private readonly container: Container) { + constructor(private readonly container: Container) { this._disposable = Disposable.from( this.container.storage.onDidChange(e => this.checkStorage(e)), lm.registerMcpServerDefinitionProvider('gitlens.mcpProvider', { diff --git a/src/env/node/providers.ts b/src/env/node/providers.ts index cac84be056cc8..98d7db21b809f 100644 --- a/src/env/node/providers.ts +++ b/src/env/node/providers.ts @@ -2,6 +2,7 @@ import type { Container } from '../../container'; import type { GitCommandOptions } from '../../git/commandOptions'; import type { GitProvider } from '../../git/gitProvider'; import type { RepositoryLocationProvider } from '../../git/location/repositorylocationProvider'; +import { supportsMcpExtensionRegistration } from '../../plus/gk/utils/-webview/mcp.utils'; import type { SharedGkStorageLocationProvider } from '../../plus/repos/sharedGkStorageLocationProvider'; import type { GkWorkspacesSharedStorageProvider } from '../../plus/workspaces/workspacesSharedStorageProvider'; import { configuration } from '../../system/-webview/configuration'; @@ -14,7 +15,7 @@ import { GkCliIntegrationProvider } from './gk/cli/integration'; import { LocalRepositoryLocationProvider } from './gk/localRepositoryLocationProvider'; import { LocalSharedGkStorageLocationProvider } from './gk/localSharedGkStorageLocationProvider'; import { LocalGkWorkspacesSharedStorageProvider } from './gk/localWorkspacesSharedStorageProvider'; -import { McpProvider } from './gk/mcp/integration'; +import type { McpProvider } from './gk/mcp/integration'; let gitInstance: Git | undefined; function ensureGit(container: Container) { @@ -73,6 +74,11 @@ export function getGkCliIntegrationProvider(container: Container): GkCliIntegrat return new GkCliIntegrationProvider(container); } -export function getMcpProvider(container: Container): McpProvider | undefined { - return McpProvider.create(container); +export async function getMcpProvider(container: Container): Promise { + if (!supportsMcpExtensionRegistration()) return undefined; + + // Older versions of VS Code do not support the classes used in the MCP integration, so we need to dynamically import + const mcpModule = await import(/* webpackChunkName: "mcp" */ './gk/mcp/integration'); + + return new mcpModule.McpProvider(container); } diff --git a/src/plus/gk/utils/-webview/mcp.utils.ts b/src/plus/gk/utils/-webview/mcp.utils.ts index 0906e9dc30bdd..2b0936f148f5f 100644 --- a/src/plus/gk/utils/-webview/mcp.utils.ts +++ b/src/plus/gk/utils/-webview/mcp.utils.ts @@ -1,6 +1,8 @@ +import { lm, version } from 'vscode'; import { getPlatform, isWeb } from '@env/platform'; import type { Container } from '../../../../container'; import { getHostAppName } from '../../../../system/-webview/vscode'; +import { satisfies } from '../../../../system/version'; export async function isMcpBannerEnabled(container: Container): Promise { // Check if running on web @@ -22,3 +24,7 @@ export async function isMcpBannerEnabled(container: Container): Promise return hostAppName != null && supportedApps.includes(hostAppName); } + +export function supportsMcpExtensionRegistration(): boolean { + return satisfies(version, '>= 1.101.0') && lm.registerMcpServerDefinitionProvider != null; +} From 647687c23d2b0b47ac99bfdd4963a21cbdf87988 Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Mon, 8 Sep 2025 13:35:59 -0400 Subject: [PATCH 26/33] Ensures mcp banners are not displayed when automatically registrable --- src/plus/gk/utils/-webview/mcp.utils.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/plus/gk/utils/-webview/mcp.utils.ts b/src/plus/gk/utils/-webview/mcp.utils.ts index 2b0936f148f5f..68ddfc25ee420 100644 --- a/src/plus/gk/utils/-webview/mcp.utils.ts +++ b/src/plus/gk/utils/-webview/mcp.utils.ts @@ -5,8 +5,8 @@ import { getHostAppName } from '../../../../system/-webview/vscode'; import { satisfies } from '../../../../system/version'; export async function isMcpBannerEnabled(container: Container): Promise { - // Check if running on web - if (isWeb) { + // Check if running on web or automatically registrable + if (isWeb || supportsMcpExtensionRegistration()) { return false; } @@ -26,5 +26,9 @@ export async function isMcpBannerEnabled(container: Container): Promise } export function supportsMcpExtensionRegistration(): boolean { + if (isWeb) { + return false; + } + return satisfies(version, '>= 1.101.0') && lm.registerMcpServerDefinitionProvider != null; } From 6a289d4a02d56a26567e84f86de09b137b719bc1 Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Mon, 8 Sep 2025 14:18:15 -0400 Subject: [PATCH 27/33] Consolidates runCLICommand --- src/env/node/gk/cli/integration.ts | 25 +++++-------------------- src/env/node/gk/cli/utils.ts | 27 +++++++++++++++++++++++++++ src/env/node/gk/mcp/integration.ts | 30 +++++------------------------- src/env/node/gk/mcp/utils.ts | 12 ------------ 4 files changed, 37 insertions(+), 57 deletions(-) create mode 100644 src/env/node/gk/cli/utils.ts delete mode 100644 src/env/node/gk/mcp/utils.ts diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index 056f151ae2f54..4eb507447600f 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -15,10 +15,10 @@ import { getLogScope } from '../../../../system/logger.scope'; import { compare } from '../../../../system/version'; import { run } from '../../git/shell'; import { getPlatform, isWeb } from '../../platform'; -import { toMcpInstallProvider } from '../mcp/utils'; import { CliCommandHandlers } from './commands'; import type { IpcServer } from './ipcServer'; import { createIpcServer } from './ipcServer'; +import { runCLICommand, toMcpInstallProvider } from './utils'; const enum CLIInstallErrorReason { UnsupportedPlatform, @@ -42,7 +42,7 @@ const CLIProxyMCPInstallOutputs = { checkingForUpdates: /checking for updates.../i, notASupportedClient: /is not a supported MCP client/i, installedSuccessfully: /GitKraken MCP Server Successfully Installed!/i, -}; +} as const; export class GkCliIntegrationProvider implements Disposable { private readonly _disposable: Disposable; @@ -238,7 +238,7 @@ export class GkCliIntegrationProvider implements Disposable { } } - let output = await this.runCLICommand( + let output = await runCLICommand( ['mcp', 'install', appName, '--source=gitlens', `--scheme=${env.uriScheme}`], { cwd: cliPath, @@ -555,7 +555,7 @@ export class GkCliIntegrationProvider implements Disposable { // Set up the local MCP server files try { - const coreInstallOutput = await this.runCLICommand(['install'], { + const coreInstallOutput = await runCLICommand(['install'], { cwd: globalStoragePath.fsPath, }); const directory = coreInstallOutput.match(/Directory: (.*)/); @@ -632,21 +632,6 @@ export class GkCliIntegrationProvider implements Disposable { return { cliVersion: cliVersion, cliPath: cliPath, status: cliInstallStatus }; } - private async runCLICommand( - args: string[], - options?: { - cwd?: string; - }, - ): Promise { - const platform = getPlatform(); - const cwd = options?.cwd ?? this.container.storage.get('gk:cli:path'); - if (cwd == null) { - throw new Error('CLI is not installed'); - } - - return run(platform === 'windows' ? 'gk.exe' : './gk', args, 'utf8', { cwd: cwd }); - } - private async authCLI(): Promise { const cliInstall = this.container.storage.get('gk:cli:install'); const cliPath = this.container.storage.get('gk:cli:path'); @@ -660,7 +645,7 @@ export class GkCliIntegrationProvider implements Disposable { } try { - await this.runCLICommand(['auth', 'login', '-t', currentSessionToken]); + await runCLICommand(['auth', 'login', '-t', currentSessionToken]); } catch (ex) { Logger.error(`Failed to auth CLI: ${ex instanceof Error ? ex.message : String(ex)}`); } diff --git a/src/env/node/gk/cli/utils.ts b/src/env/node/gk/cli/utils.ts new file mode 100644 index 0000000000000..4a371c1ffe55f --- /dev/null +++ b/src/env/node/gk/cli/utils.ts @@ -0,0 +1,27 @@ +import { Container } from '../../../../container'; +import { run } from '../../git/shell'; +import { getPlatform } from '../../platform'; + +export function toMcpInstallProvider(appHostName: string | undefined): string | undefined { + switch (appHostName) { + case 'code': + return 'vscode'; + case 'code-insiders': + return 'vscode-insiders'; + case 'code-exploration': + return 'vscode-exploration'; + default: + return appHostName; + } +} + +export async function runCLICommand(args: string[], options?: { cwd?: string }): Promise { + const cwd = options?.cwd ?? Container.instance.storage.get('gk:cli:path'); + if (cwd == null) { + throw new Error('CLI is not installed'); + } + + const platform = getPlatform(); + + return run(platform === 'windows' ? 'gk.exe' : './gk', args, 'utf8', { cwd: cwd }); +} diff --git a/src/env/node/gk/mcp/integration.ts b/src/env/node/gk/mcp/integration.ts index ca42ba39ffc80..c695f2abb8a8d 100644 --- a/src/env/node/gk/mcp/integration.ts +++ b/src/env/node/gk/mcp/integration.ts @@ -5,13 +5,11 @@ import type { StorageChangeEvent } from '../../../../system/-webview/storage'; import { getHostAppName } from '../../../../system/-webview/vscode'; import { debounce } from '../../../../system/function/debounce'; import { Logger } from '../../../../system/logger'; -import { run } from '../../git/shell'; -import { getPlatform } from '../../platform'; -import { toMcpInstallProvider } from './utils'; +import { runCLICommand, toMcpInstallProvider } from '../cli/utils'; const CLIProxyMCPConfigOutputs = { checkingForUpdates: /checking for updates.../i, -}; +} as const; export class McpProvider implements Disposable { private readonly _disposable: Disposable; @@ -96,12 +94,9 @@ export class McpProvider implements Disposable { return undefined; } - let output = await this.runCLICommand( - ['mcp', 'config', appName, '--source=gitlens', `--scheme=${env.uriScheme}`], - { - cwd: cliPath, - }, - ); + let output = await runCLICommand(['mcp', 'config', appName, '--source=gitlens', `--scheme=${env.uriScheme}`], { + cwd: cliPath, + }); output = output.replace(CLIProxyMCPConfigOutputs.checkingForUpdates, '').trim(); console.log(output); @@ -122,21 +117,6 @@ export class McpProvider implements Disposable { return undefined; } - private async runCLICommand( - args: string[], - options?: { - cwd?: string; - }, - ): Promise { - const platform = getPlatform(); - const cwd = options?.cwd ?? this.container.storage.get('gk:cli:path'); - if (cwd == null) { - throw new Error('CLI is not installed'); - } - - return run(platform === 'windows' ? 'gk.exe' : './gk', args, 'utf8', { cwd: cwd }); - } - dispose(): void { this._disposable.dispose(); this._onDidChangeMcpServerDefinitions.dispose(); diff --git a/src/env/node/gk/mcp/utils.ts b/src/env/node/gk/mcp/utils.ts deleted file mode 100644 index 01917816831f3..0000000000000 --- a/src/env/node/gk/mcp/utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function toMcpInstallProvider(appHostName: string | undefined): string | undefined { - switch (appHostName) { - case 'code': - return 'vscode'; - case 'code-insiders': - return 'vscode-insiders'; - case 'code-exploration': - return 'vscode-exploration'; - default: - return appHostName; - } -} From 83104d0fac59dd1ceb0a7c8ed51a3cf4d193f9a3 Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Mon, 8 Sep 2025 16:44:38 -0400 Subject: [PATCH 28/33] Removes unused code --- src/env/node/gk/mcp/integration.ts | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/src/env/node/gk/mcp/integration.ts b/src/env/node/gk/mcp/integration.ts index c695f2abb8a8d..95e521bdd5577 100644 --- a/src/env/node/gk/mcp/integration.ts +++ b/src/env/node/gk/mcp/integration.ts @@ -54,31 +54,6 @@ export class McpProvider implements Disposable { return [serverDefinition]; } - // private async getMcpConfiguration(): Promise< - // { name: string; type: string; command: string; args: string[]; version?: string } | undefined - // > { - // const cliInstall = this.container.storage.get('gk:cli:install'); - // const cliPath = this.container.storage.get('gk:cli:path'); - - // if (cliInstall?.status !== 'completed' || !cliPath) { - // return undefined; - // } - - // const platform = getPlatform(); - // const executable = platform === 'windows' ? 'gk.exe' : 'gk'; - // const command = Uri.joinPath(Uri.file(cliPath), executable); - - // const appName = toMcpInstallProvider(await getHostAppName()); - // const args = ['mcp', `--host=${appName}`, '--source=gitlens', `--scheme=${env.uriScheme}`]; - // return { - // name: 'GitKraken MCP Server', - // type: 'stdio', - // command: command.fsPath, - // args: args, - // version: cliInstall.version, - // }; - // } - private async getMcpConfigurationFromCLI(): Promise< { name: string; type: string; command: string; args: string[]; version?: string } | undefined > { @@ -98,7 +73,6 @@ export class McpProvider implements Disposable { cwd: cliPath, }); output = output.replace(CLIProxyMCPConfigOutputs.checkingForUpdates, '').trim(); - console.log(output); try { const configuration = JSON.parse(output) as { name: string; type: string; command: string; args: string[] }; From b3ab1c095c05b85896cb4af1f456795cb45f1f2f Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Tue, 9 Sep 2025 16:38:23 -0400 Subject: [PATCH 29/33] Updates GkMcpProvider for mcp registration - updates contribution label and id - re-enables CLI auto-install behind setting --- package.json | 14 ++++++++++++-- src/config.ts | 3 +++ src/container.ts | 12 ++++++------ src/env/browser/providers.ts | 5 +++-- src/env/node/gk/cli/integration.ts | 20 ++++++++++++++------ src/env/node/gk/mcp/integration.ts | 13 +++++-------- src/env/node/providers.ts | 8 +++++--- 7 files changed, 48 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 811a50b5e4126..25051be205a8a 100644 --- a/package.json +++ b/package.json @@ -69,8 +69,8 @@ "contributes": { "mcpServerDefinitionProviders": [ { - "id": "gitlens.mcpProvider", - "label": "GitLens MCP Provider" + "id": "gitlens.gkMcpProvider", + "label": "GitKraken (bundled with GitLens)" } ], "configuration": [ @@ -4111,6 +4111,16 @@ "experimental" ] }, + "gitlens.gitkraken.cli.autoInstall.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "Specifies whether to automatically install the GitKraken CLI", + "scope": "window", + "order": 40, + "tags": [ + "experimental" + ] + }, "gitlens.terminal.overrideGitEditor": { "type": "boolean", "default": true, diff --git a/src/config.ts b/src/config.ts index df694b6b4f063..f28d595d89703 100644 --- a/src/config.ts +++ b/src/config.ts @@ -395,6 +395,9 @@ interface GitKrakenCliConfig { readonly integration: { readonly enabled: boolean; }; + readonly autoInstall: { + readonly enabled: boolean; + }; } export interface GraphConfig { diff --git a/src/container.ts b/src/container.ts index 131d9d095813f..9df704e5d432d 100644 --- a/src/container.ts +++ b/src/container.ts @@ -2,7 +2,7 @@ import type { ConfigurationChangeEvent, Disposable, Event, ExtensionContext } fr import { EventEmitter, ExtensionMode } from 'vscode'; import { getGkCliIntegrationProvider, - getMcpProvider, + getMcpProviders, getSharedGKStorageLocationProvider, getSupportedGitProviders, getSupportedRepositoryLocationProvider, @@ -320,7 +320,7 @@ export class Container { this._ready = true; await this.registerGitProviders(); - await this.registerMcpProvider(); + await this.registerMcpProviders(); queueMicrotask(() => this._onReady.fire()); } @@ -336,10 +336,10 @@ export class Container { } @log() - private async registerMcpProvider(): Promise { - const mcpProvider = await getMcpProvider(this); - if (mcpProvider != null) { - this._disposables.push(mcpProvider); + private async registerMcpProviders(): Promise { + const mcpProviders = await getMcpProviders(this); + if (mcpProviders != null) { + this._disposables.push(...mcpProviders); } } diff --git a/src/env/browser/providers.ts b/src/env/browser/providers.ts index d5403929a789d..b52ceb5739fb3 100644 --- a/src/env/browser/providers.ts +++ b/src/env/browser/providers.ts @@ -1,3 +1,4 @@ +import type { Disposable } from 'vscode'; import type { Container } from '../../container'; import type { GitCommandOptions } from '../../git/commandOptions'; // Force import of GitHub since dynamic imports are not supported in the WebWorker ExtensionHost @@ -43,6 +44,6 @@ export function getGkCliIntegrationProvider(_container: Container): undefined { return undefined; } -export function getMcpProvider(_container: Container): undefined { - return undefined; +export function getMcpProviders(_container: Container): Promise { + return Promise.resolve(undefined); } diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index 4eb507447600f..a44dc4983de90 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -5,6 +5,7 @@ import { urls } from '../../../../constants'; import type { Source, Sources } from '../../../../constants.telemetry'; import type { Container } from '../../../../container'; import type { SubscriptionChangeEvent } from '../../../../plus/gk/subscriptionService'; +import { supportsMcpExtensionRegistration } from '../../../../plus/gk/utils/-webview/mcp.utils'; import { registerCommand } from '../../../../system/-webview/command'; import { configuration } from '../../../../system/-webview/configuration'; import { getHostAppName } from '../../../../system/-webview/vscode'; @@ -57,11 +58,13 @@ export class GkCliIntegrationProvider implements Disposable { this.onConfigurationChanged(); - // TODO: Uncomment this once we feel confident enough that the install process is stable cross-platform - /* const cliInstall = this.container.storage.get('gk:cli:install'); - if (!cliInstall || (cliInstall.status === 'attempted' && cliInstall.attempts < 5)) { - setTimeout(() => this.installCLI(true), 10000 + Math.floor(Math.random() * 20000)); - } */ + // TODO: Remove this experimental setting for production release + if (configuration.get('gitkraken.cli.autoInstall.enabled')) { + const cliInstall = this.container.storage.get('gk:cli:install'); + if (!cliInstall || (cliInstall.status === 'attempted' && cliInstall.attempts < 5)) { + setTimeout(() => this.installCLI(true), 10000 + Math.floor(Math.random() * 20000)); + } + } } dispose(): void { @@ -219,6 +222,11 @@ export class GkCliIntegrationProvider implements Disposable { return; } + // If MCP extension registration is supported, don't proceed with manual setup + if (supportsMcpExtensionRegistration()) { + return; + } + if (appName !== 'cursor' && appName !== 'vscode' && appName !== 'vscode-insiders') { const confirmation = await window.showInformationMessage( `GitKraken MCP installed successfully. Click 'Finish' to add it to your MCP server list and complete the setup.`, @@ -268,7 +276,7 @@ export class GkCliIntegrationProvider implements Disposable { const learnMore = { title: 'View Setup Instructions' }; const cancel = { title: 'Cancel', isCloseAffordance: true }; const result = await window.showInformationMessage( - 'This application doesn’t support automatic MCP setup. Please add the GitKraken MCP to your configuration manually.', + "This application doesn't support automatic MCP setup. Please add the GitKraken MCP to your configuration manually.", { modal: true }, learnMore, cancel, diff --git a/src/env/node/gk/mcp/integration.ts b/src/env/node/gk/mcp/integration.ts index 95e521bdd5577..ee44dfce57f9d 100644 --- a/src/env/node/gk/mcp/integration.ts +++ b/src/env/node/gk/mcp/integration.ts @@ -1,4 +1,4 @@ -import type { Event, McpServerDefinition } from 'vscode'; +import type { Event, McpServerDefinition, McpServerDefinitionProvider } from 'vscode'; import { Disposable, env, EventEmitter, lm, McpStdioServerDefinition, window } from 'vscode'; import type { Container } from '../../../../container'; import type { StorageChangeEvent } from '../../../../system/-webview/storage'; @@ -11,7 +11,7 @@ const CLIProxyMCPConfigOutputs = { checkingForUpdates: /checking for updates.../i, } as const; -export class McpProvider implements Disposable { +export class GkMcpProvider implements McpServerDefinitionProvider, Disposable { private readonly _disposable: Disposable; private readonly _onDidChangeMcpServerDefinitions = new EventEmitter(); get onDidChangeMcpServerDefinitions(): Event { @@ -21,10 +21,7 @@ export class McpProvider implements Disposable { constructor(private readonly container: Container) { this._disposable = Disposable.from( this.container.storage.onDidChange(e => this.checkStorage(e)), - lm.registerMcpServerDefinitionProvider('gitlens.mcpProvider', { - onDidChangeMcpServerDefinitions: this._onDidChangeMcpServerDefinitions.event, - provideMcpServerDefinitions: () => this.provideMcpServerDefinitions(), - }), + lm.registerMcpServerDefinitionProvider('gitlens.gkMcpProvider', this), ); this.checkStorage(); @@ -35,14 +32,14 @@ export class McpProvider implements Disposable { this._onDidChangeMcpServerDefinitions.fire(); } - private async provideMcpServerDefinitions(): Promise { + async provideMcpServerDefinitions(): Promise { const config = await this.getMcpConfigurationFromCLI(); if (config == null) { return []; } const serverDefinition = new McpStdioServerDefinition( - config.name, + `${config.name} (bundled with GitLens)`, config.command, config.args, {}, diff --git a/src/env/node/providers.ts b/src/env/node/providers.ts index 98d7db21b809f..1ce41d0d51d16 100644 --- a/src/env/node/providers.ts +++ b/src/env/node/providers.ts @@ -1,3 +1,4 @@ +import type { Disposable, McpServerDefinitionProvider } from 'vscode'; import type { Container } from '../../container'; import type { GitCommandOptions } from '../../git/commandOptions'; import type { GitProvider } from '../../git/gitProvider'; @@ -15,7 +16,6 @@ import { GkCliIntegrationProvider } from './gk/cli/integration'; import { LocalRepositoryLocationProvider } from './gk/localRepositoryLocationProvider'; import { LocalSharedGkStorageLocationProvider } from './gk/localSharedGkStorageLocationProvider'; import { LocalGkWorkspacesSharedStorageProvider } from './gk/localWorkspacesSharedStorageProvider'; -import type { McpProvider } from './gk/mcp/integration'; let gitInstance: Git | undefined; function ensureGit(container: Container) { @@ -74,11 +74,13 @@ export function getGkCliIntegrationProvider(container: Container): GkCliIntegrat return new GkCliIntegrationProvider(container); } -export async function getMcpProvider(container: Container): Promise { +export async function getMcpProviders( + container: Container, +): Promise<(McpServerDefinitionProvider & Disposable)[] | undefined> { if (!supportsMcpExtensionRegistration()) return undefined; // Older versions of VS Code do not support the classes used in the MCP integration, so we need to dynamically import const mcpModule = await import(/* webpackChunkName: "mcp" */ './gk/mcp/integration'); - return new mcpModule.McpProvider(container); + return [new mcpModule.GkMcpProvider(container)]; } From f8cf7b444700a377d6285adb5d0f25e69d198185 Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Wed, 10 Sep 2025 13:55:25 -0400 Subject: [PATCH 30/33] Updates banner when gk mcp can be auto registered --- src/plus/gk/utils/-webview/mcp.utils.ts | 4 +-- src/webviews/apps/home/home.ts | 1 + src/webviews/apps/home/stateProvider.ts | 3 +- .../shared/components/banner/banner.css.ts | 2 ++ .../apps/shared/components/mcp-banner.ts | 28 +++++++++++++++++-- src/webviews/home/homeWebview.ts | 14 ++++++++-- src/webviews/home/protocol.ts | 7 ++++- 7 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/plus/gk/utils/-webview/mcp.utils.ts b/src/plus/gk/utils/-webview/mcp.utils.ts index 68ddfc25ee420..04f7c0942fdb0 100644 --- a/src/plus/gk/utils/-webview/mcp.utils.ts +++ b/src/plus/gk/utils/-webview/mcp.utils.ts @@ -4,9 +4,9 @@ import type { Container } from '../../../../container'; import { getHostAppName } from '../../../../system/-webview/vscode'; import { satisfies } from '../../../../system/version'; -export async function isMcpBannerEnabled(container: Container): Promise { +export async function isMcpBannerEnabled(container: Container, showAutoRegistration = false): Promise { // Check if running on web or automatically registrable - if (isWeb || supportsMcpExtensionRegistration()) { + if (isWeb || (!showAutoRegistration && supportsMcpExtensionRegistration())) { return false; } diff --git a/src/webviews/apps/home/home.ts b/src/webviews/apps/home/home.ts index cffdd287002ec..e0d3fa26f5d77 100644 --- a/src/webviews/apps/home/home.ts +++ b/src/webviews/apps/home/home.ts @@ -92,6 +92,7 @@ export class GlHomeApp extends GlAppHost { diff --git a/src/webviews/apps/home/stateProvider.ts b/src/webviews/apps/home/stateProvider.ts index 6bdecb72ef056..75a96ddef68d1 100644 --- a/src/webviews/apps/home/stateProvider.ts +++ b/src/webviews/apps/home/stateProvider.ts @@ -97,7 +97,8 @@ export class HomeStateProvider implements StateProvider { host.requestUpdate(); break; case DidChangeMcpBanner.is(msg): - this._state.mcpBannerCollapsed = msg.params; + this._state.mcpBannerCollapsed = msg.params.mcpBannerCollapsed; + this._state.mcpCanAutoRegister = msg.params.mcpCanAutoRegister; this._state.timestamp = Date.now(); this.provider.setValue(this._state, true); diff --git a/src/webviews/apps/shared/components/banner/banner.css.ts b/src/webviews/apps/shared/components/banner/banner.css.ts index 8894e8d5e2720..97efcda71045c 100644 --- a/src/webviews/apps/shared/components/banner/banner.css.ts +++ b/src/webviews/apps/shared/components/banner/banner.css.ts @@ -212,6 +212,7 @@ export const bannerStyles = css` font-weight: bold; color: var(--gl-banner-text-color); margin: 0; + text-wrap: pretty; } .banner__body { @@ -219,6 +220,7 @@ export const bannerStyles = css` color: var(--gl-banner-text-color); margin: 0; line-height: 1.4; + text-wrap: pretty; } .banner__buttons { diff --git a/src/webviews/apps/shared/components/mcp-banner.ts b/src/webviews/apps/shared/components/mcp-banner.ts index c40bc78e757fb..ede11fc7577d2 100644 --- a/src/webviews/apps/shared/components/mcp-banner.ts +++ b/src/webviews/apps/shared/components/mcp-banner.ts @@ -1,5 +1,5 @@ import { css, html, LitElement, nothing } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; +import { customElement, property } from 'lit/decorators.js'; import { urls } from '../../../../constants'; import { createCommandLink } from '../../../../system/commands'; import './banner/banner'; @@ -40,14 +40,36 @@ export class GlMcpBanner extends LitElement { @property() layout: 'default' | 'responsive' = 'default'; - @state() - private collapsed: boolean = false; + @property({ type: Boolean }) + collapsed: boolean = false; + + @property({ type: Boolean }) + private canAutoRegister: boolean = false; override render(): unknown { if (this.collapsed) { return nothing; } + if (this.canAutoRegister) { + const bodyHtml = `GitKraken MCP is now active in Copilot chat. Ask Copilot to "start work on issue PROJ-123" or "create a PR for my commits" to see Git workflows powered by AI. Learn more`; + + return html` + + `; + } + const bodyHtml = `Leverage Git and Integration information from GitLens in AI chat. Learn more`; return html` diff --git a/src/webviews/home/homeWebview.ts b/src/webviews/home/homeWebview.ts index 6ea92cb5c9646..8611ce5325348 100644 --- a/src/webviews/home/homeWebview.ts +++ b/src/webviews/home/homeWebview.ts @@ -51,7 +51,7 @@ import type { AIModelChangeEvent } from '../../plus/ai/aiProviderService'; import { showPatchesView } from '../../plus/drafts/actions'; import type { Subscription } from '../../plus/gk/models/subscription'; import type { SubscriptionChangeEvent } from '../../plus/gk/subscriptionService'; -import { isMcpBannerEnabled } from '../../plus/gk/utils/-webview/mcp.utils'; +import { isMcpBannerEnabled, supportsMcpExtensionRegistration } from '../../plus/gk/utils/-webview/mcp.utils'; import { isAiAllAccessPromotionActive } from '../../plus/gk/utils/-webview/promo.utils'; import { isSubscriptionTrialOrPaidFromState } from '../../plus/gk/utils/subscription.utils'; import type { ConfiguredIntegrationsChangeEvent } from '../../plus/integrations/authentication/configuredIntegrationService'; @@ -803,7 +803,11 @@ export class HomeWebviewProvider implements WebviewProvider(scop export const DismissWalkthroughSection = new IpcCommand(scope, 'walkthrough/dismiss'); export const DidChangeAiAllAccessBanner = new IpcNotification(scope, 'ai/allAccess/didChange'); -export const DidChangeMcpBanner = new IpcNotification(scope, 'mcp/didChange'); +export interface DidChangeMcpBannerParams { + mcpBannerCollapsed: boolean; + mcpCanAutoRegister: boolean; +} +export const DidChangeMcpBanner = new IpcNotification(scope, 'mcp/didChange'); export const DismissAiAllAccessBannerCommand = new IpcCommand(scope, 'ai/allAccess/dismiss'); export const SetOverviewFilter = new IpcCommand(scope, 'overview/filter/set'); From cc31725762f8e3684a65bce84a2f9c1cbae84438 Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Wed, 10 Sep 2025 13:59:27 -0400 Subject: [PATCH 31/33] Removes unneeded mcp server registration attempt --- src/env/node/gk/mcp/integration.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/env/node/gk/mcp/integration.ts b/src/env/node/gk/mcp/integration.ts index ee44dfce57f9d..298ad686388e1 100644 --- a/src/env/node/gk/mcp/integration.ts +++ b/src/env/node/gk/mcp/integration.ts @@ -23,8 +23,6 @@ export class GkMcpProvider implements McpServerDefinitionProvider, Disposable { this.container.storage.onDidChange(e => this.checkStorage(e)), lm.registerMcpServerDefinitionProvider('gitlens.gkMcpProvider', this), ); - - this.checkStorage(); } private checkStorage(e?: StorageChangeEvent): void { From 29aae3da9a0c546cd616a2f88a1f295c77416d64 Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Wed, 10 Sep 2025 14:02:30 -0400 Subject: [PATCH 32/33] Updates mcp provider telemetry and removes unwanted toast --- docs/telemetry-events.md | 46 +++++++++++++++--------------- src/constants.telemetry.ts | 2 ++ src/env/node/gk/cli/integration.ts | 5 +++- src/env/node/gk/mcp/integration.ts | 39 +++++++++++++++++-------- 4 files changed, 56 insertions(+), 36 deletions(-) diff --git a/docs/telemetry-events.md b/docs/telemetry-events.md index 617032d918e05..614e92ccdc2ba 100644 --- a/docs/telemetry-events.md +++ b/docs/telemetry-events.md @@ -579,7 +579,7 @@ void 'attempts': number, 'autoInstall': boolean, 'error.message': string, - 'source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees' + 'source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees' } ``` @@ -591,7 +591,7 @@ void { 'attempts': number, 'autoInstall': boolean, - 'source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees' + 'source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees' } ``` @@ -603,7 +603,7 @@ void { 'attempts': number, 'autoInstall': boolean, - 'source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'version': string } ``` @@ -1002,7 +1002,7 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, 'context.webview.host': 'view' | 'editor', @@ -1057,7 +1057,7 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, 'context.webview.host': 'view' | 'editor', @@ -1112,7 +1112,7 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, 'context.webview.host': 'view' | 'editor', @@ -1167,7 +1167,7 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, 'context.webview.host': 'view' | 'editor', @@ -1222,7 +1222,7 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, 'context.webview.host': 'view' | 'editor', @@ -1277,7 +1277,7 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, 'context.webview.host': 'view' | 'editor', @@ -1332,7 +1332,7 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, 'context.webview.host': 'view' | 'editor', @@ -1387,7 +1387,7 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, 'context.webview.host': 'view' | 'editor', @@ -1442,7 +1442,7 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, 'context.webview.host': 'view' | 'editor', @@ -1497,7 +1497,7 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, 'context.webview.host': 'view' | 'editor', @@ -1552,7 +1552,7 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, 'context.webview.host': 'view' | 'editor', @@ -1607,7 +1607,7 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, 'context.webview.host': 'view' | 'editor', @@ -1674,7 +1674,7 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, 'context.webview.host': 'view' | 'editor', @@ -1729,7 +1729,7 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, 'context.webview.host': 'view' | 'editor', @@ -1811,7 +1811,7 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, 'context.webview.host': 'view' | 'editor', @@ -1866,7 +1866,7 @@ or 'context.operations.undo.count': number, 'context.session.duration': number, 'context.session.start': string, - 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', + 'context.source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees', 'context.warnings.indexChanged': boolean, 'context.warnings.workingDirectoryChanged': boolean, 'context.webview.host': 'view' | 'editor', @@ -2772,7 +2772,7 @@ void { 'cli.version': string, 'requiresUserCompletion': boolean, - 'source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees' + 'source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees' } ``` @@ -2785,7 +2785,7 @@ void 'cli.version': string, 'error.message': string, 'reason': string, - 'source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees' + 'source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees' } ``` @@ -2795,7 +2795,7 @@ void ```typescript { - 'source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees' + 'source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees' } ``` @@ -2811,7 +2811,7 @@ void 'repoPrivacy': 'private' | 'public' | 'local', 'repository.visibility': 'private' | 'public' | 'local', // Provided for compatibility with other GK surfaces - 'source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees' + 'source': 'account' | 'subscription' | 'graph' | 'composer' | 'patchDetails' | 'settings' | 'timeline' | 'home' | 'ai' | 'ai:markdown-preview' | 'ai:markdown-editor' | 'ai:picker' | 'associateIssueWithBranch' | 'cloud-patches' | 'code-suggest' | 'commandPalette' | 'deeplink' | 'editor:hover' | 'feature-badge' | 'feature-gate' | 'gk-cli-integration' | 'gk-mcp-provider' | 'inspect' | 'inspect-overview' | 'integrations' | 'launchpad' | 'launchpad-indicator' | 'launchpad-view' | 'merge-target' | 'notification' | 'prompt' | 'quick-wizard' | 'rebaseEditor' | 'remoteProvider' | 'scm' | 'scm-input' | 'startWork' | 'trial-indicator' | 'view' | 'walkthrough' | 'whatsnew' | 'worktrees' } ``` diff --git a/src/constants.telemetry.ts b/src/constants.telemetry.ts index 9f2be7120d463..e8c5eb8137ff3 100644 --- a/src/constants.telemetry.ts +++ b/src/constants.telemetry.ts @@ -1220,6 +1220,8 @@ export type Sources = | 'editor:hover' | 'feature-badge' | 'feature-gate' + | 'gk-cli-integration' + | 'gk-mcp-provider' | 'graph' | 'home' | 'inspect' diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index a44dc4983de90..f28d5a73922f1 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -62,7 +62,10 @@ export class GkCliIntegrationProvider implements Disposable { if (configuration.get('gitkraken.cli.autoInstall.enabled')) { const cliInstall = this.container.storage.get('gk:cli:install'); if (!cliInstall || (cliInstall.status === 'attempted' && cliInstall.attempts < 5)) { - setTimeout(() => this.installCLI(true), 10000 + Math.floor(Math.random() * 20000)); + setTimeout( + () => this.installCLI(true, 'gk-cli-integration'), + 10000 + Math.floor(Math.random() * 20000), + ); } } } diff --git a/src/env/node/gk/mcp/integration.ts b/src/env/node/gk/mcp/integration.ts index 298ad686388e1..6f2a3199b213d 100644 --- a/src/env/node/gk/mcp/integration.ts +++ b/src/env/node/gk/mcp/integration.ts @@ -1,9 +1,9 @@ import type { Event, McpServerDefinition, McpServerDefinitionProvider } from 'vscode'; -import { Disposable, env, EventEmitter, lm, McpStdioServerDefinition, window } from 'vscode'; +import { Disposable, env, EventEmitter, lm, McpStdioServerDefinition } from 'vscode'; import type { Container } from '../../../../container'; import type { StorageChangeEvent } from '../../../../system/-webview/storage'; import { getHostAppName } from '../../../../system/-webview/vscode'; -import { debounce } from '../../../../system/function/debounce'; +import { log } from '../../../../system/decorators/log'; import { Logger } from '../../../../system/logger'; import { runCLICommand, toMcpInstallProvider } from '../cli/utils'; @@ -44,11 +44,10 @@ export class GkMcpProvider implements McpServerDefinitionProvider, Disposable { config.version, ); - this.notifyServerProvided(); - return [serverDefinition]; } + @log() private async getMcpConfigurationFromCLI(): Promise< { name: string; type: string; command: string; args: string[]; version?: string } | undefined > { @@ -72,6 +71,8 @@ export class GkMcpProvider implements McpServerDefinitionProvider, Disposable { try { const configuration = JSON.parse(output) as { name: string; type: string; command: string; args: string[] }; + this.notifySetupCompleted(cliInstall.version); + return { name: configuration.name, type: configuration.type, @@ -81,21 +82,35 @@ export class GkMcpProvider implements McpServerDefinitionProvider, Disposable { }; } catch (ex) { Logger.error(`Error getting MCP configuration: ${ex}`); + this.notifySetupFailed('Error getting MCP configuration', undefined, cliInstall.version); } return undefined; } + private notifySetupCompleted(cliVersion?: string | undefined) { + if (!this.container.telemetry.enabled) return; + + this.container.telemetry.sendEvent('mcp/setup/completed', { + requiresUserCompletion: false, + source: 'gk-mcp-provider', + 'cli.version': cliVersion, + }); + } + + private notifySetupFailed(reason: string, message?: string | undefined, cliVersion?: string | undefined) { + if (!this.container.telemetry.enabled) return; + + this.container.telemetry.sendEvent('mcp/setup/failed', { + reason: reason, + 'error.message': message, + source: 'gk-mcp-provider', + 'cli.version': cliVersion, + }); + } + dispose(): void { this._disposable.dispose(); this._onDidChangeMcpServerDefinitions.dispose(); } - - private _notifyServerProvided = false; - private notifyServerProvided = debounce(() => { - if (this._notifyServerProvided) return; - - void window.showInformationMessage('GitLens can now automatically configure the GitKraken MCP server for you'); - this._notifyServerProvided = true; - }, 250); } From 602773c5fd6edabd2072a447a928fe146a4fa7e4 Mon Sep 17 00:00:00 2001 From: Keith Daulton Date: Wed, 10 Sep 2025 15:07:34 -0400 Subject: [PATCH 33/33] Updates checks for cli and mcp auto installation --- src/env/node/gk/cli/integration.ts | 8 +++++--- src/plus/gk/utils/-webview/mcp.utils.ts | 12 +++++++++++- src/webviews/home/homeWebview.ts | 4 ++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/env/node/gk/cli/integration.ts b/src/env/node/gk/cli/integration.ts index f28d5a73922f1..e825595f4ecc7 100644 --- a/src/env/node/gk/cli/integration.ts +++ b/src/env/node/gk/cli/integration.ts @@ -5,7 +5,10 @@ import { urls } from '../../../../constants'; import type { Source, Sources } from '../../../../constants.telemetry'; import type { Container } from '../../../../container'; import type { SubscriptionChangeEvent } from '../../../../plus/gk/subscriptionService'; -import { supportsMcpExtensionRegistration } from '../../../../plus/gk/utils/-webview/mcp.utils'; +import { + mcpExtensionRegistrationAllowed, + supportsMcpExtensionRegistration, +} from '../../../../plus/gk/utils/-webview/mcp.utils'; import { registerCommand } from '../../../../system/-webview/command'; import { configuration } from '../../../../system/-webview/configuration'; import { getHostAppName } from '../../../../system/-webview/vscode'; @@ -58,8 +61,7 @@ export class GkCliIntegrationProvider implements Disposable { this.onConfigurationChanged(); - // TODO: Remove this experimental setting for production release - if (configuration.get('gitkraken.cli.autoInstall.enabled')) { + if (mcpExtensionRegistrationAllowed()) { const cliInstall = this.container.storage.get('gk:cli:install'); if (!cliInstall || (cliInstall.status === 'attempted' && cliInstall.attempts < 5)) { setTimeout( diff --git a/src/plus/gk/utils/-webview/mcp.utils.ts b/src/plus/gk/utils/-webview/mcp.utils.ts index 04f7c0942fdb0..2903ee85dede8 100644 --- a/src/plus/gk/utils/-webview/mcp.utils.ts +++ b/src/plus/gk/utils/-webview/mcp.utils.ts @@ -1,12 +1,13 @@ import { lm, version } from 'vscode'; import { getPlatform, isWeb } from '@env/platform'; import type { Container } from '../../../../container'; +import { configuration } from '../../../../system/-webview/configuration'; import { getHostAppName } from '../../../../system/-webview/vscode'; import { satisfies } from '../../../../system/version'; export async function isMcpBannerEnabled(container: Container, showAutoRegistration = false): Promise { // Check if running on web or automatically registrable - if (isWeb || (!showAutoRegistration && supportsMcpExtensionRegistration())) { + if (isWeb || (!showAutoRegistration && mcpExtensionRegistrationAllowed())) { return false; } @@ -32,3 +33,12 @@ export function supportsMcpExtensionRegistration(): boolean { return satisfies(version, '>= 1.101.0') && lm.registerMcpServerDefinitionProvider != null; } + +export function mcpExtensionRegistrationAllowed(): boolean { + // TODO: Remove experimental setting for production release + return ( + configuration.get('ai.enabled') && + configuration.get('gitkraken.cli.autoInstall.enabled') && + supportsMcpExtensionRegistration() + ); +} diff --git a/src/webviews/home/homeWebview.ts b/src/webviews/home/homeWebview.ts index 8611ce5325348..dd9b2b27dfd3c 100644 --- a/src/webviews/home/homeWebview.ts +++ b/src/webviews/home/homeWebview.ts @@ -51,7 +51,7 @@ import type { AIModelChangeEvent } from '../../plus/ai/aiProviderService'; import { showPatchesView } from '../../plus/drafts/actions'; import type { Subscription } from '../../plus/gk/models/subscription'; import type { SubscriptionChangeEvent } from '../../plus/gk/subscriptionService'; -import { isMcpBannerEnabled, supportsMcpExtensionRegistration } from '../../plus/gk/utils/-webview/mcp.utils'; +import { isMcpBannerEnabled, mcpExtensionRegistrationAllowed } from '../../plus/gk/utils/-webview/mcp.utils'; import { isAiAllAccessPromotionActive } from '../../plus/gk/utils/-webview/promo.utils'; import { isSubscriptionTrialOrPaidFromState } from '../../plus/gk/utils/subscription.utils'; import type { ConfiguredIntegrationsChangeEvent } from '../../plus/integrations/authentication/configuredIntegrationService'; @@ -807,7 +807,7 @@ export class HomeWebviewProvider implements WebviewProvider