diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 0534b87029a5e..e5a8716b1ea28 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2297,17 +2297,17 @@ export namespace InteractiveEditorResponseFeedbackKind { export namespace ChatResponseProgress { export function from(extension: IExtensionDescription, progress: vscode.ChatAgentExtendedProgress): extHostProtocol.IChatProgressDto { if ('placeholder' in progress && 'resolvedContent' in progress) { - return { placeholder: progress.placeholder, kind: 'asyncContent' } satisfies extHostProtocol.IChatAsyncContentDto; + return { content: progress.placeholder, kind: 'asyncContent' } satisfies extHostProtocol.IChatAsyncContentDto; } else if ('markdownContent' in progress) { checkProposedApiEnabled(extension, 'chatAgents2Additions'); - return { content: MarkdownString.from(progress.markdownContent), kind: 'content' }; + return { content: MarkdownString.from(progress.markdownContent), kind: 'markdownContent' }; } else if ('content' in progress) { if (typeof progress.content === 'string') { return { content: progress.content, kind: 'content' }; } checkProposedApiEnabled(extension, 'chatAgents2Additions'); - return { content: MarkdownString.from(progress.content), kind: 'content' }; + return { content: MarkdownString.from(progress.content), kind: 'markdownContent' }; } else if ('documents' in progress) { return { documents: progress.documents.map(d => ({ diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 91810918bd39d..b316f8d5dfcd0 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -45,7 +45,7 @@ import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { ChatWelcomeMessageModel } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; +import { IMarkdownString, MarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; import { ChatProviderService, IChatProviderService } from 'vs/workbench/contrib/chat/common/chatProvider'; import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { alertFocusChange } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions'; @@ -243,7 +243,11 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { const defaultAgent = chatAgentService.getDefaultAgent(); const agents = chatAgentService.getAgents(); if (defaultAgent?.metadata.helpTextPrefix) { - progress.report({ content: defaultAgent.metadata.helpTextPrefix, kind: 'content' }); + if (isMarkdownString(defaultAgent.metadata.helpTextPrefix)) { + progress.report({ content: defaultAgent.metadata.helpTextPrefix, kind: 'markdownContent' }); + } else { + progress.report({ content: defaultAgent.metadata.helpTextPrefix, kind: 'content' }); + } progress.report({ content: '\n\n', kind: 'content' }); } @@ -263,10 +267,14 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { return agentLine + '\n' + commandText; }))).join('\n'); - progress.report({ content: new MarkdownString(agentText, { isTrusted: { enabledCommands: [SubmitAction.ID] } }), kind: 'content' }); + progress.report({ content: new MarkdownString(agentText, { isTrusted: { enabledCommands: [SubmitAction.ID] } }), kind: 'markdownContent' }); if (defaultAgent?.metadata.helpTextPostfix) { progress.report({ content: '\n\n', kind: 'content' }); - progress.report({ content: defaultAgent.metadata.helpTextPostfix, kind: 'content' }); + if (isMarkdownString(defaultAgent.metadata.helpTextPostfix)) { + progress.report({ content: defaultAgent.metadata.helpTextPostfix, kind: 'markdownContent' }); + } else { + progress.report({ content: defaultAgent.metadata.helpTextPostfix, kind: 'content' }); + } } })); } diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 44ffc0e2571b7..bae45cad52d92 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -55,9 +55,9 @@ import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions import { CodeBlockPart, ICodeBlockData, ICodeBlockPart } from 'vs/workbench/contrib/chat/browser/codeBlockPart'; import { IChatAgentMetadata } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { IPlaceholderMarkdownString } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatContentReference, IChatReplyFollowup, IChatResponseProgressFileTreeData, IChatService, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatContentInlineReference, IChatContentReference, IChatReplyFollowup, IChatResponseProgressFileTreeData, IChatService, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatResponseMarkdownRenderData, IChatResponseRenderData, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/files/browser/views/explorerView'; @@ -319,7 +319,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer, element: ChatTreeItem, index: number, templateData: IChatListItemTemplate) { + private basicRenderElement(value: ReadonlyArray>, element: ChatTreeItem, index: number, templateData: IChatListItemTemplate) { const fillInIncompleteTokens = isResponseVM(element) && (!element.isComplete || element.isCanceled || element.errorDetails?.responseIsFiltered || element.errorDetails?.responseIsIncomplete); dom.clearNode(templateData.value); @@ -414,9 +414,11 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer): ReadonlyArray { - const result: (IMarkdownString | IChatResponseProgressFileTreeData)[] = []; +export function reduceInlineContentReferences(response: ReadonlyArray): ReadonlyArray> { + const result: Exclude[] = []; for (const item of response) { const previousItem = result[result.length - 1]; - if ('inlineReference' in item) { + if (item.kind === 'inlineReference') { const location = 'uri' in item.inlineReference ? item.inlineReference : { uri: item.inlineReference }; const printUri = URI.parse(contentRefUrl).with({ fragment: JSON.stringify(location) }); const markdownText = `[${item.name || basename(location.uri)}](${printUri.toString()})`; - if (isMarkdownString(previousItem)) { - result[result.length - 1] = new MarkdownString(previousItem.value + markdownText, { isTrusted: previousItem.isTrusted }); + if (previousItem?.kind === 'markdownContent') { + result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + markdownText, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' }; } else { - result.push(new MarkdownString(markdownText)); + result.push({ content: new MarkdownString(markdownText), kind: 'markdownContent' }); } - } else if (isMarkdownString(item) && isMarkdownString(previousItem)) { - result[result.length - 1] = new MarkdownString(previousItem.value + item.value, { isTrusted: previousItem.isTrusted }); + } else if (item.kind === 'markdownContent' && previousItem?.kind === 'markdownContent') { + result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + item.content.value, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' }; } else { result.push(item); } diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 7ed5082fefe08..16b3db9787308 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { firstOrDefault } from 'vs/base/common/arrays'; +import { asArray, firstOrDefault } from 'vs/base/common/arrays'; import { DeferredPromise } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { IMarkdownString, MarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; @@ -16,7 +16,7 @@ import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { ILogService } from 'vs/platform/log/common/log'; import { IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChat, IChatAgentDetection, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatProgress, IChatReplyFollowup, IChatResponse, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatUsedContext, InteractiveSessionVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChat, IChatAsyncContent, IChatContent, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatReplyFollowup, IChatResponse, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, IChatTreeData, IChatUsedContext, InteractiveSessionVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; export interface IChatRequestModel { readonly id: string; @@ -27,10 +27,14 @@ export interface IChatRequestModel { readonly response: IChatResponseModel | undefined; } -export type IChatProgressResponseContent = Exclude; +export type IChatProgressResponseContent = + | IChatMarkdownContent + | IChatTreeData + | IChatAsyncContent + | IChatContentInlineReference; export interface IResponse { - readonly value: ReadonlyArray; + readonly value: ReadonlyArray; asString(): string; } @@ -80,15 +84,6 @@ export class ChatRequestModel implements IChatRequestModel { } } -export interface IPlaceholderMarkdownString extends IMarkdownString { - isPlaceholder: boolean; -} - -type InternalResponsePart = - | { string: IMarkdownString; isPlaceholder?: boolean } - | IChatContentInlineReference - | { treeData: IChatResponseProgressFileTreeData; isPlaceholder?: undefined }; - export class Response implements IResponse { private _onDidChangeValue = new Emitter(); public get onDidChangeValue() { @@ -96,107 +91,86 @@ export class Response implements IResponse { } // responseParts internally tracks all the response parts, including strings which are currently resolving, so that they can be updated when they do resolve - private _responseParts: InternalResponsePart[]; - // responseData externally presents the response parts with consolidated contiguous strings (including strings which were previously resolving) - private _responseData: (IMarkdownString | IPlaceholderMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference)[]; + private _responseParts: IChatProgressResponseContent[]; // responseRepr externally presents the response parts with consolidated contiguous strings (excluding tree data) - private _responseRepr: string; + private _responseRepr!: string; - get value(): (IMarkdownString | IPlaceholderMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference)[] { - return this._responseData; + get value(): IChatProgressResponseContent[] { + return this._responseParts; } constructor(value: IMarkdownString | ReadonlyArray) { - this._responseData = Array.isArray(value) ? value : [value]; - this._responseParts = Array.isArray(value) ? value.map((v) => ('value' in v ? { string: v } : { treeData: v })) : [{ string: value }]; - this._responseRepr = this._responseParts.map((part) => { - if (isCompleteInteractiveProgressTreeData(part)) { - return ''; - } - // TODO duplicates _updateRepr - if ('inlineReference' in part) { - return basename('uri' in part.inlineReference ? part.inlineReference.uri : part.inlineReference); - } - return part.string.value; - }).join('\n'); + this._responseParts = asArray(value).map((v) => (isMarkdownString(v) ? + { content: v, kind: 'markdownContent' } satisfies IChatMarkdownContent : + 'kind' in v ? v : { kind: 'treeData', treeData: v })); + + this._updateRepr(true); } asString(): string { return this._responseRepr; } - updateContent(progress: IChatProgressResponseContent, quiet?: boolean): void { - if (progress.kind === 'content') { + updateContent(progress: IChatProgressResponseContent | IChatContent, quiet?: boolean): void { + if (progress.kind === 'content' || progress.kind === 'markdownContent') { const responsePartLength = this._responseParts.length - 1; const lastResponsePart = this._responseParts[responsePartLength]; - if (lastResponsePart && ('inlineReference' in lastResponsePart || lastResponsePart.isPlaceholder === true || isCompleteInteractiveProgressTreeData(lastResponsePart))) { - // The last part is resolving or a tree data item, start a new part - this._responseParts.push({ string: typeof progress.content === 'string' ? new MarkdownString(progress.content) : progress.content }); - } else if (lastResponsePart) { - // Combine this part with the last, non-resolving string part - if (isMarkdownString(progress.content)) { - // Merge all enabled commands - const lastPartEnabledCommands = typeof lastResponsePart.string.isTrusted === 'object' ? lastResponsePart.string.isTrusted.enabledCommands : []; - const thisPartEnabledCommands = typeof progress.content.isTrusted === 'object' ? progress.content.isTrusted.enabledCommands : []; - const enabledCommands = [...lastPartEnabledCommands, ...thisPartEnabledCommands]; - this._responseParts[responsePartLength] = { string: new MarkdownString(lastResponsePart.string.value + progress.content.value, { isTrusted: { enabledCommands } }) }; + if (!lastResponsePart || lastResponsePart.kind !== 'markdownContent') { + // The last part can't be merged with + if (progress.kind === 'content') { + this._responseParts.push({ content: new MarkdownString(progress.content), kind: 'markdownContent' }); } else { - this._responseParts[responsePartLength] = { string: new MarkdownString(lastResponsePart.string.value + progress.content, lastResponsePart.string) }; + this._responseParts.push(progress); } + } else if (progress.kind === 'markdownContent') { + // Merge all enabled commands + const lastPartEnabledCommands = typeof lastResponsePart.content.isTrusted === 'object' ? + lastResponsePart.content.isTrusted.enabledCommands : + []; + const thisPartEnabledCommands = typeof progress.content.isTrusted === 'object' ? + progress.content.isTrusted.enabledCommands : + []; + const enabledCommands = [...lastPartEnabledCommands, ...thisPartEnabledCommands]; + this._responseParts[responsePartLength] = { content: new MarkdownString(lastResponsePart.content.value + progress.content.value, { isTrusted: { enabledCommands } }), kind: 'markdownContent' }; } else { - this._responseParts.push({ string: isMarkdownString(progress.content) ? progress.content : new MarkdownString(progress.content) }); + this._responseParts[responsePartLength] = { content: new MarkdownString(lastResponsePart.content.value + progress.content, lastResponsePart.content), kind: 'markdownContent' }; } this._updateRepr(quiet); - } else if ('placeholder' in progress) { + } else if (progress.kind === 'asyncContent') { // Add a new resolving part - const responsePosition = this._responseParts.push({ string: new MarkdownString(progress.placeholder), isPlaceholder: true }) - 1; + const responsePosition = this._responseParts.push(progress) - 1; this._updateRepr(quiet); progress.resolvedContent?.then((content) => { // Replace the resolving part's content with the resolved response if (typeof content === 'string') { - this._responseParts[responsePosition] = { string: new MarkdownString(content), isPlaceholder: true }; - this._updateRepr(quiet); - } else if ('value' in content) { - this._responseParts[responsePosition] = { string: content, isPlaceholder: true }; - this._updateRepr(quiet); - } else if (content.treeData) { - this._responseParts[responsePosition] = { treeData: content.treeData }; - this._updateRepr(quiet); + this._responseParts[responsePosition] = { content: new MarkdownString(content), kind: 'markdownContent' }; + } else if (isMarkdownString(content)) { + this._responseParts[responsePosition] = { content, kind: 'markdownContent' }; + } else { + this._responseParts[responsePosition] = content; } + this._updateRepr(quiet); }); - } else if (isCompleteInteractiveProgressTreeData(progress)) { - this._responseParts.push(progress); - this._updateRepr(quiet); - } else if ('inlineReference' in progress) { + } else if (progress.kind === 'treeData' || progress.kind === 'inlineReference') { this._responseParts.push(progress); this._updateRepr(quiet); } } private _updateRepr(quiet?: boolean) { - this._responseData = this._responseParts.map(part => { - if ('inlineReference' in part) { - return part; - } else if (isCompleteInteractiveProgressTreeData(part)) { - return part.treeData; - } else if (part.isPlaceholder) { - return { ...part.string, isPlaceholder: true }; - } - return part.string; - }); - this._responseRepr = this._responseParts.map(part => { - if (isCompleteInteractiveProgressTreeData(part)) { + if (part.kind === 'treeData') { return ''; - } - if ('inlineReference' in part) { + } else if (part.kind === 'inlineReference') { return basename('uri' in part.inlineReference ? part.inlineReference.uri : part.inlineReference); + } else if (part.kind === 'asyncContent') { + return part.content; + } else { + return part.content.value; } - - return part.string.value; }).join('\n\n'); if (!quiet) { @@ -295,7 +269,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel this._id = 'response_' + ChatResponseModel.nextId++; } - updateContent(responsePart: IChatProgressResponseContent, quiet?: boolean) { + updateContent(responsePart: IChatProgressResponseContent | IChatContent, quiet?: boolean) { this._response.updateContent(responsePart, quiet); } @@ -652,17 +626,17 @@ export class ChatModel extends Disposable implements IChatModel { throw new Error('acceptResponseProgress: Adding progress to a completed response'); } - if (progress.kind === 'content') { - request.response.updateContent(progress, quiet); - } else if ('placeholder' in progress || isCompleteInteractiveProgressTreeData(progress) || 'inlineReference' in progress) { + if (progress.kind === 'content' || progress.kind === 'markdownContent' || progress.kind === 'asyncContent' || progress.kind === 'treeData' || progress.kind === 'inlineReference') { request.response.updateContent(progress, quiet); - } else if ('documents' in progress || 'reference' in progress) { + } else if (progress.kind === 'usedContext' || progress.kind === 'reference') { request.response.updateReferences(progress); - } else if ('agentName' in progress) { + } else if (progress.kind === 'agentDetection') { const agent = this.chatAgentService.getAgent(progress.agentName); if (agent) { request.response.setAgent(agent, progress.command); } + } else { + this.logService.error(`Couldn't handle progress: ${JSON.stringify(progress)}`); } } @@ -733,7 +707,20 @@ export class ChatModel extends Disposable implements IChatModel { requests: this._requests.map((r): ISerializableChatRequestData => { return { message: r.message, - response: r.response ? r.response.response.value : undefined, + response: r.response ? + r.response.response.value.map(item => { + // Keeping the shape of the persisted data the same for back compat + if (item.kind === 'treeData') { + return item.treeData; + } else if (item.kind === 'markdownContent') { + return item.content; + } else if (item.kind === 'asyncContent') { + return new MarkdownString(item.content); + } else { + return item; + } + }) + : undefined, responseErrorDetails: r.response?.errorDetails, followups: r.response?.followups, isCanceled: r.response?.isCanceled, @@ -801,7 +788,3 @@ export class ChatWelcomeMessageModel implements IChatWelcomeMessageModel { return this.session.responderAvatarIconUri; } } - -export function isCompleteInteractiveProgressTreeData(item: unknown): item is { treeData: IChatResponseProgressFileTreeData } { - return typeof item === 'object' && !!item && 'treeData' in item; -} diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 095f81c1c73cf..6494613b2cd96 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -102,23 +102,32 @@ export interface IChatAgentDetection { } export interface IChatContent { - content: string | IMarkdownString; + content: string; kind: 'content'; } +export interface IChatMarkdownContent { + content: IMarkdownString; + kind: 'markdownContent'; +} + export interface IChatTreeData { treeData: IChatResponseProgressFileTreeData; kind: 'treeData'; } export interface IChatAsyncContent { - placeholder: string; + /** + * The placeholder to show while the content is loading + */ + content: string; resolvedContent: Promise; kind: 'asyncContent'; } export type IChatProgress = | IChatContent + | IChatMarkdownContent | IChatTreeData | IChatAsyncContent | IChatUsedContext @@ -258,7 +267,7 @@ export interface IChatDynamicRequest { } export interface IChatCompleteResponse { - message: string | ReadonlyArray; + message: string | ReadonlyArray; errorDetails?: IChatResponseErrorDetails; followups?: IChatFollowup[]; } diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 1e3ef878ce75d..9d447d5bbfc96 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -6,7 +6,7 @@ import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { MarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; +import { MarkdownString } from 'vs/base/common/htmlContent'; import { Iterable } from 'vs/base/common/iterator'; import { Disposable, DisposableMap, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; @@ -469,7 +469,7 @@ export class ChatService extends Disposable implements IChatService { gotProgress = true; - if (progress.kind === 'content') { + if (progress.kind === 'content' || progress.kind === 'markdownContent') { this.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${typeof progress.content === 'string' ? progress.content.length : progress.content.value.length} chars`); } else { this.trace('sendRequest', `Provider returned progress: ${JSON.stringify(progress)}`); @@ -646,10 +646,7 @@ export class ChatService extends Disposable implements IChatService { model.acceptResponseProgress(request, { content: response.message, kind: 'content' }); } else { for (const part of response.message) { - const progress: IChatProgress = 'inlineReference' in part ? part : - isMarkdownString(part) ? { content: part.value, kind: 'content' } : - { treeData: part, kind: 'treeData' }; - model.acceptResponseProgress(request, progress, true); + model.acceptResponseProgress(request, part, true); } } model.setResponse(request, { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_async_content.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_async_content.0.snap new file mode 100644 index 0000000000000..2abf0a346b4d4 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_async_content.0.snap @@ -0,0 +1,12 @@ +[ + { + resolvedContent: { }, + content: "text", + kind: "asyncContent" + }, + { + resolvedContent: { }, + content: "text2", + kind: "asyncContent" + } +] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_async_content.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_async_content.1.snap new file mode 100644 index 0000000000000..9a2a0ee7bb26f --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_async_content.1.snap @@ -0,0 +1,26 @@ +[ + { + content: { + value: "resolved", + isTrusted: false, + supportThemeIcons: false, + supportHtml: false + }, + kind: "markdownContent" + }, + { + kind: "treeData", + treeData: { + label: "label", + uri: { + scheme: "https", + authority: "microsoft.com", + path: "/", + query: "", + fragment: "", + _formatted: null, + _fsPath: null + } + } + } +] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_content__markdown.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_content__markdown.0.snap new file mode 100644 index 0000000000000..b923c5ded8442 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_content__markdown.0.snap @@ -0,0 +1,11 @@ +[ + { + content: { + value: "textmarkdown", + isTrusted: { enabledCommands: [ ] }, + supportThemeIcons: false, + supportHtml: false + }, + kind: "markdownContent" + } +] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_inline_reference.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_inline_reference.0.snap new file mode 100644 index 0000000000000..5847d813dfe49 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_inline_reference.0.snap @@ -0,0 +1,32 @@ +[ + { + content: { + value: "text before", + isTrusted: false, + supportThemeIcons: false, + supportHtml: false + }, + kind: "markdownContent" + }, + { + inlineReference: { + scheme: "https", + authority: "microsoft.com", + path: "/", + query: "", + fragment: "", + _formatted: null, + _fsPath: null + }, + kind: "inlineReference" + }, + { + content: { + value: "text after", + isTrusted: false, + supportThemeIcons: false, + supportHtml: false + }, + kind: "markdownContent" + } +] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_markdown__content.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_markdown__content.0.snap new file mode 100644 index 0000000000000..0643f935d8f47 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_markdown__content.0.snap @@ -0,0 +1,11 @@ +[ + { + content: { + value: "markdowntext", + isTrusted: false, + supportThemeIcons: false, + supportHtml: false + }, + kind: "markdownContent" + } +] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts index 56d9cf5d323d7..33f0e1d0a0622 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts @@ -4,16 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { timeout } from 'vs/base/common/async'; +import { DeferredPromise, timeout } from 'vs/base/common/async'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { URI } from 'vs/base/common/uri'; +import { assertSnapshot } from 'vs/base/test/common/snapshot'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { Range } from 'vs/editor/common/core/range'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; +import { Range } from 'vs/editor/common/core/range'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { ChatModel, Response } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestTextPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { IChatTreeData } from 'vs/workbench/contrib/chat/common/chatService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; @@ -116,3 +120,43 @@ suite('ChatModel', () => { assert.strictEqual(model.getRequests().length, 0); }); }); + +suite('Response', () => { + test('content, markdown', async () => { + const response = new Response([]); + response.updateContent({ content: 'text', kind: 'content' }); + response.updateContent({ content: new MarkdownString('markdown'), kind: 'markdownContent' }); + await assertSnapshot(response.value); + + assert.strictEqual(response.asString(), 'textmarkdown'); + }); + + test('markdown, content', async () => { + const response = new Response([]); + response.updateContent({ content: new MarkdownString('markdown'), kind: 'markdownContent' }); + response.updateContent({ content: 'text', kind: 'content' }); + await assertSnapshot(response.value); + }); + + test('async content', async () => { + const response = new Response([]); + const deferred = new DeferredPromise(); + const deferred2 = new DeferredPromise(); + response.updateContent({ resolvedContent: deferred.p, content: 'text', kind: 'asyncContent' }); + response.updateContent({ resolvedContent: deferred2.p, content: 'text2', kind: 'asyncContent' }); + await assertSnapshot(response.value); + + await deferred2.complete({ kind: 'treeData', treeData: { label: 'label', uri: URI.parse('https://microsoft.com') } }); + await deferred.complete('resolved'); + await assertSnapshot(response.value); + }); + + + test('inline reference', async () => { + const response = new Response([]); + response.updateContent({ content: 'text before', kind: 'content' }); + response.updateContent({ inlineReference: URI.parse('https://microsoft.com'), kind: 'inlineReference' }); + response.updateContent({ content: 'text after', kind: 'content' }); + await assertSnapshot(response.value); + }); +});