From f114cfb18a79d49197b5101f71b619e3a413bc23 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 5 Feb 2025 17:14:33 -0800 Subject: [PATCH 1/2] issue reporter download extension data if file too big --- .../chat/browser/chatPasteProviders.ts | 75 +++++++++++++++---- .../issue/browser/baseIssueReporterService.ts | 43 ++++++++++- .../issue/browser/issueReporterPage.ts | 1 + .../issue/browser/issueReporterService.ts | 10 ++- .../electron-sandbox/issueReporterService.ts | 11 ++- 5 files changed, 117 insertions(+), 23 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts b/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts index 5550d3d33b9cb..c5fc6f95b7671 100644 --- a/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts +++ b/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts @@ -2,29 +2,40 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { createStringDataTransferItem, IDataTransferItem, IReadonlyVSDataTransfer, VSDataTransfer } from '../../../../base/common/dataTransfer.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { createFileDataTransferItem, createStringDataTransferItem, IDataTransferItem, IReadonlyVSDataTransfer, VSDataTransfer } from '../../../../base/common/dataTransfer.js'; import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Mimes } from '../../../../base/common/mime.js'; +import { basename } from '../../../../base/common/resources.js'; +import { URI, UriComponents } from '../../../../base/common/uri.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteEditsSession } from '../../../../editor/common/languages.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { ChatInputPart } from './chatInputPart.js'; -import { IChatWidgetService } from './chat.js'; -import { Codicon } from '../../../../base/common/codicons.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; import { localize } from '../../../../nls.js'; -import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry } from '../common/chatModel.js'; import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js'; -import { Mimes } from '../../../../base/common/mime.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; -import { URI, UriComponents } from '../../../../base/common/uri.js'; -import { basename } from '../../../../base/common/resources.js'; +import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry } from '../common/chatModel.js'; +import { IChatWidgetService } from './chat.js'; +import { ChatInputPart } from './chatInputPart.js'; import { resizeImage } from './imageUtils.js'; const COPY_MIME_TYPES = 'application/vnd.code.additional-editor-data'; +enum CopyFilesSettings { + Never = 'never', + MediaFiles = 'mediaFiles', +} + +export enum MediaKind { + Image = 1, + Video, + Audio +} + + interface SerializedCopyData { readonly uri: UriComponents; readonly range: IRange; @@ -90,12 +101,16 @@ export class PasteImageProvider implements DocumentPasteEditProvider { tempDisplayName = `${displayName} ${appendValue}`; } + const fileReference = await this.createFileForMedia(imageItem, mimeType, CopyFilesSettings.MediaFiles, token); + if (token.isCancellationRequested || !fileReference) { + return; + } const scaledImageData = await resizeImage(currClipboard); if (token.isCancellationRequested || !scaledImageData) { return; } - const scaledImageContext = await getImageAttachContext(scaledImageData, mimeType, token, tempDisplayName); + const scaledImageContext = await getImageAttachContext(scaledImageData, mimeType, token, tempDisplayName, fileReference); if (token.isCancellationRequested || !scaledImageContext) { return; } @@ -111,9 +126,40 @@ export class PasteImageProvider implements DocumentPasteEditProvider { const edit = createCustomPasteEdit(model, scaledImageContext, mimeType, this.kind, localize('pastedImageAttachment', 'Pasted Image Attachment'), this.chatWidgetService); return createEditSession(edit); } + + private async createFileForMedia( + dataTransfer: IDataTransferItem, + mimeType: string, + copyIntoWorkspace: CopyFilesSettings, + token: CancellationToken, + ): Promise { + const file = dataTransfer.asFile(); + if (!file) { + return; + } + + if (file.uri) { + // check if already exists or not + } + + const data = file.data(); + const virtualUri = URI.from({ + scheme: 'untitled', + path: '/virtual-file.txt' + }); + + const newFile = createFileDataTransferItem(file.name, virtualUri, async () => await data); + const createdFile = newFile.asFile(); + + if (!createdFile) { + return; + } + + return createdFile.uri; + } } -async function getImageAttachContext(data: Uint8Array, mimeType: string, token: CancellationToken, displayName: string): Promise { +async function getImageAttachContext(data: Uint8Array, mimeType: string, token: CancellationToken, displayName: string, resource: URI): Promise { const imageHash = await imageToHash(data); if (token.isCancellationRequested) { return undefined; @@ -126,7 +172,8 @@ async function getImageAttachContext(data: Uint8Array, mimeType: string, token: isImage: true, icon: Codicon.fileMedia, isDynamic: true, - mimeType + mimeType, + references: [{ reference: resource, kind: 'reference' }] }; } diff --git a/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts b/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts index 4e0b80d97f8dd..5b141801e762a 100644 --- a/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts +++ b/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts @@ -8,25 +8,35 @@ import { Button, unthemedButtonStyles } from '../../../../base/browser/ui/button import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { mainWindow } from '../../../../base/browser/window.js'; import { Delayer, RunOnceScheduler } from '../../../../base/common/async.js'; +import { VSBuffer } from '../../../../base/common/buffer.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { groupBy } from '../../../../base/common/collections.js'; import { debounce } from '../../../../base/common/decorators.js'; import { CancellationError } from '../../../../base/common/errors.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../base/common/network.js'; import { isLinuxSnap, isMacintosh } from '../../../../base/common/platform.js'; import { IProductConfiguration } from '../../../../base/common/product.js'; +import { joinPath } from '../../../../base/common/resources.js'; import { escape } from '../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; +import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; import { getIconsStyleSheet } from '../../../../platform/theme/browser/iconsStyleSheet.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { IssueReporterModel, IssueReporterData as IssueReporterModelData } from './issueReporterModel.js'; import { IIssueFormService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles, IssueType } from '../common/issue.js'; import { normalizeGitHubUrl } from '../common/issueReporterUtil.js'; +import { IssueReporterModel, IssueReporterData as IssueReporterModelData } from './issueReporterModel.js'; const MAX_URL_LENGTH = 7500; +// Github API and issues on web has a limit of 65536. If extension data is too large, we will allow users to downlaod and attach it as a file. +// We round down to be safe. +// ref https://github.com/github/issues/issues/12858 +const MAX_EXTENSION_DATA_LENGTH = 55000; + interface SearchResult { html_url: string; title: string; @@ -68,6 +78,8 @@ export class BaseIssueReporterService extends Disposable { public readonly isWeb: boolean, @IIssueFormService public readonly issueFormService: IIssueFormService, @IThemeService public readonly themeService: IThemeService, + @IFileService public readonly fileService: IFileService, + @IFileDialogService public readonly fileDialogService: IFileDialogService, ) { super(); const targetExtension = data.extensionId ? data.enabledExtensions.find(extension => extension.id.toLocaleLowerCase() === data.extensionId?.toLocaleLowerCase()) : undefined; @@ -861,7 +873,7 @@ export class BaseIssueReporterService extends Disposable { } } - public renderBlocks(): void { + public async renderBlocks(): Promise { // Depending on Issue Type, we render different blocks and text const { issueType, fileOnExtension, fileOnMarketplace, selectedExtension } = this.issueReporterModel.getData(); const blockContainer = this.getElementById('block-container'); @@ -876,6 +888,7 @@ export class BaseIssueReporterService extends Disposable { const descriptionTitle = this.getElementById('issue-description-label')!; const descriptionSubtitle = this.getElementById('issue-description-subtitle')!; const extensionSelector = this.getElementById('extension-selection')!; + const downloadExtensionDataLink = this.getElementById('extension-data-download')!; const titleTextArea = this.getElementById('issue-title-container')!; const descriptionTextArea = this.getElementById('description')!; @@ -891,6 +904,7 @@ export class BaseIssueReporterService extends Disposable { hide(extensionSelector); hide(extensionDataTextArea); hide(extensionDataBlock); + hide(downloadExtensionDataLink); show(problemSource); show(titleTextArea); @@ -900,6 +914,31 @@ export class BaseIssueReporterService extends Disposable { show(extensionSelector); } + const extensionData = this.issueReporterModel.getData().extensionData; + if (extensionData && extensionData.length > MAX_EXTENSION_DATA_LENGTH) { + show(downloadExtensionDataLink); + const date = new Date(); + const formattedDate = date.toISOString().split('T')[0]; // YYYY-MM-DD + const formattedTime = date.toTimeString().split(' ')[0].replace(/:/g, '-'); // HH-MM-SS + const fileName = `extensionData_${formattedDate}_${formattedTime}.md`; + const handleLinkClick = async () => { + const downloadPath = await this.fileDialogService.showSaveDialog({ + title: localize('saveExtensionData', "Save Extension Data"), + availableFileSystems: [Schemas.file], + defaultUri: joinPath(await this.fileDialogService.defaultFilePath(Schemas.file), fileName), + }); + + if (downloadPath) { + await this.fileService.writeFile(downloadPath, VSBuffer.fromString(extensionData)); + } + }; + + downloadExtensionDataLink.addEventListener('click', handleLinkClick); + + this._register({ + dispose: () => downloadExtensionDataLink.removeEventListener('click', handleLinkClick) + }); + } if (selectedExtension && this.nonGitHubIssueUrl) { hide(titleTextArea); diff --git a/src/vs/workbench/contrib/issue/browser/issueReporterPage.ts b/src/vs/workbench/contrib/issue/browser/issueReporterPage.ts index 5cbc70936ffb2..fbd7fbb3fb8d7 100644 --- a/src/vs/workbench/contrib/issue/browser/issueReporterPage.ts +++ b/src/vs/workbench/contrib/issue/browser/issueReporterPage.ts @@ -93,6 +93,7 @@ export default (): string => ` ${sendExtensionData} ${escape(localize('show', "show"))} + ${escape(localize('downloadExtensionData', "Download Extension Data"))}
 				
diff --git a/src/vs/workbench/contrib/issue/browser/issueReporterService.ts b/src/vs/workbench/contrib/issue/browser/issueReporterService.ts
index ae2d6106f7572..abfc9943e1754 100644
--- a/src/vs/workbench/contrib/issue/browser/issueReporterService.ts
+++ b/src/vs/workbench/contrib/issue/browser/issueReporterService.ts
@@ -4,9 +4,11 @@
  *--------------------------------------------------------------------------------------------*/
 import { IProductConfiguration } from '../../../../base/common/product.js';
 import { localize } from '../../../../nls.js';
+import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
+import { IFileService } from '../../../../platform/files/common/files.js';
 import { IThemeService } from '../../../../platform/theme/common/themeService.js';
-import { BaseIssueReporterService } from './baseIssueReporterService.js';
 import { IIssueFormService, IssueReporterData } from '../common/issue.js';
+import { BaseIssueReporterService } from './baseIssueReporterService.js';
 
 // GitHub has let us know that we could up our limit here to 8k. We chose 7500 to play it safe.
 // ref https://github.com/microsoft/vscode/issues/159191
@@ -23,9 +25,11 @@ export class IssueWebReporter extends BaseIssueReporterService {
 		product: IProductConfiguration,
 		window: Window,
 		@IIssueFormService issueFormService: IIssueFormService,
-		@IThemeService themeService: IThemeService
+		@IThemeService themeService: IThemeService,
+		@IFileService fileService: IFileService,
+		@IFileDialogService fileDialogService: IFileDialogService
 	) {
-		super(disableExtensions, data, os, product, window, true, issueFormService, themeService);
+		super(disableExtensions, data, os, product, window, true, issueFormService, themeService, fileService, fileDialogService);
 
 		const target = this.window.document.querySelector('.block-system .block-info');
 
diff --git a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts
index bc6b696e10489..a21991a5d0b17 100644
--- a/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts
+++ b/src/vs/workbench/contrib/issue/electron-sandbox/issueReporterService.ts
@@ -8,8 +8,10 @@ import { IProductConfiguration } from '../../../../base/common/product.js';
 import { URI } from '../../../../base/common/uri.js';
 import { localize } from '../../../../nls.js';
 import { isRemoteDiagnosticError } from '../../../../platform/diagnostics/common/diagnostics.js';
-import { IProcessMainService } from '../../../../platform/process/common/process.js';
+import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
+import { IFileService } from '../../../../platform/files/common/files.js';
 import { INativeHostService } from '../../../../platform/native/common/native.js';
+import { IProcessMainService } from '../../../../platform/process/common/process.js';
 import { IThemeService } from '../../../../platform/theme/common/themeService.js';
 import { applyZoom } from '../../../../platform/window/electron-sandbox/window.js';
 import { BaseIssueReporterService } from '../browser/baseIssueReporterService.js';
@@ -40,9 +42,11 @@ export class IssueReporter extends BaseIssueReporterService {
 		@INativeHostService private readonly nativeHostService: INativeHostService,
 		@IIssueFormService issueFormService: IIssueFormService,
 		@IProcessMainService processMainService: IProcessMainService,
-		@IThemeService themeService: IThemeService
+		@IThemeService themeService: IThemeService,
+		@IFileService fileService: IFileService,
+		@IFileDialogService fileDialogService: IFileDialogService
 	) {
-		super(disableExtensions, data, os, product, window, false, issueFormService, themeService);
+		super(disableExtensions, data, os, product, window, false, issueFormService, themeService, fileService, fileDialogService);
 		this.processMainService = processMainService;
 		this.processMainService.$getSystemInfo().then(info => {
 			this.issueReporterModel.update({ systemInfo: info });
@@ -90,7 +94,6 @@ export class IssueReporter extends BaseIssueReporterService {
 
 	public override async submitToGitHub(issueTitle: string, issueBody: string, gitHubDetails: { owner: string; repositoryName: string }): Promise {
 		if (issueBody.length > MAX_GITHUB_API_LENGTH) {
-			console.error('Issue body is too long.');
 			return false;
 		}
 		const url = `https://api.github.com/repos/${gitHubDetails.owner}/${gitHubDetails.repositoryName}/issues`;

From 1affda3f2a0ff9481df32cad9a15339d44073c4e Mon Sep 17 00:00:00 2001
From: Your Name 
Date: Wed, 5 Feb 2025 17:16:13 -0800
Subject: [PATCH 2/2] revert chat past providers

---
 .../chat/browser/chatPasteProviders.ts        | 75 ++++---------------
 1 file changed, 14 insertions(+), 61 deletions(-)

diff --git a/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts b/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts
index c5fc6f95b7671..5550d3d33b9cb 100644
--- a/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts
+++ b/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts
@@ -2,40 +2,29 @@
  *  Copyright (c) Microsoft Corporation. All rights reserved.
  *  Licensed under the MIT License. See License.txt in the project root for license information.
  *--------------------------------------------------------------------------------------------*/
+
 import { CancellationToken } from '../../../../base/common/cancellation.js';
-import { Codicon } from '../../../../base/common/codicons.js';
-import { createFileDataTransferItem, createStringDataTransferItem, IDataTransferItem, IReadonlyVSDataTransfer, VSDataTransfer } from '../../../../base/common/dataTransfer.js';
+import { createStringDataTransferItem, IDataTransferItem, IReadonlyVSDataTransfer, VSDataTransfer } from '../../../../base/common/dataTransfer.js';
 import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js';
-import { Disposable } from '../../../../base/common/lifecycle.js';
-import { Mimes } from '../../../../base/common/mime.js';
-import { basename } from '../../../../base/common/resources.js';
-import { URI, UriComponents } from '../../../../base/common/uri.js';
 import { IRange } from '../../../../editor/common/core/range.js';
 import { DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteEditsSession } from '../../../../editor/common/languages.js';
 import { ITextModel } from '../../../../editor/common/model.js';
 import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
-import { IModelService } from '../../../../editor/common/services/model.js';
+import { Disposable } from '../../../../base/common/lifecycle.js';
+import { ChatInputPart } from './chatInputPart.js';
+import { IChatWidgetService } from './chat.js';
+import { Codicon } from '../../../../base/common/codicons.js';
 import { localize } from '../../../../nls.js';
-import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js';
 import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry } from '../common/chatModel.js';
-import { IChatWidgetService } from './chat.js';
-import { ChatInputPart } from './chatInputPart.js';
+import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js';
+import { Mimes } from '../../../../base/common/mime.js';
+import { IModelService } from '../../../../editor/common/services/model.js';
+import { URI, UriComponents } from '../../../../base/common/uri.js';
+import { basename } from '../../../../base/common/resources.js';
 import { resizeImage } from './imageUtils.js';
 
 const COPY_MIME_TYPES = 'application/vnd.code.additional-editor-data';
 
-enum CopyFilesSettings {
-	Never = 'never',
-	MediaFiles = 'mediaFiles',
-}
-
-export enum MediaKind {
-	Image = 1,
-	Video,
-	Audio
-}
-
-
 interface SerializedCopyData {
 	readonly uri: UriComponents;
 	readonly range: IRange;
@@ -101,16 +90,12 @@ export class PasteImageProvider implements DocumentPasteEditProvider {
 			tempDisplayName = `${displayName} ${appendValue}`;
 		}
 
-		const fileReference = await this.createFileForMedia(imageItem, mimeType, CopyFilesSettings.MediaFiles, token);
-		if (token.isCancellationRequested || !fileReference) {
-			return;
-		}
 		const scaledImageData = await resizeImage(currClipboard);
 		if (token.isCancellationRequested || !scaledImageData) {
 			return;
 		}
 
-		const scaledImageContext = await getImageAttachContext(scaledImageData, mimeType, token, tempDisplayName, fileReference);
+		const scaledImageContext = await getImageAttachContext(scaledImageData, mimeType, token, tempDisplayName);
 		if (token.isCancellationRequested || !scaledImageContext) {
 			return;
 		}
@@ -126,40 +111,9 @@ export class PasteImageProvider implements DocumentPasteEditProvider {
 		const edit = createCustomPasteEdit(model, scaledImageContext, mimeType, this.kind, localize('pastedImageAttachment', 'Pasted Image Attachment'), this.chatWidgetService);
 		return createEditSession(edit);
 	}
-
-	private async createFileForMedia(
-		dataTransfer: IDataTransferItem,
-		mimeType: string,
-		copyIntoWorkspace: CopyFilesSettings,
-		token: CancellationToken,
-	): Promise {
-		const file = dataTransfer.asFile();
-		if (!file) {
-			return;
-		}
-
-		if (file.uri) {
-			// check if already exists or not
-		}
-
-		const data = file.data();
-		const virtualUri = URI.from({
-			scheme: 'untitled',
-			path: '/virtual-file.txt'
-		});
-
-		const newFile = createFileDataTransferItem(file.name, virtualUri, async () => await data);
-		const createdFile = newFile.asFile();
-
-		if (!createdFile) {
-			return;
-		}
-
-		return createdFile.uri;
-	}
 }
 
-async function getImageAttachContext(data: Uint8Array, mimeType: string, token: CancellationToken, displayName: string, resource: URI): Promise {
+async function getImageAttachContext(data: Uint8Array, mimeType: string, token: CancellationToken, displayName: string): Promise {
 	const imageHash = await imageToHash(data);
 	if (token.isCancellationRequested) {
 		return undefined;
@@ -172,8 +126,7 @@ async function getImageAttachContext(data: Uint8Array, mimeType: string, token:
 		isImage: true,
 		icon: Codicon.fileMedia,
 		isDynamic: true,
-		mimeType,
-		references: [{ reference: resource, kind: 'reference' }]
+		mimeType
 	};
 }