diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index fdc2c217f9c..3b1577444a8 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1735,6 +1735,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseProgressPart: extHostTypes.ChatResponseProgressPart, ChatResponseProgressPart2: extHostTypes.ChatResponseProgressPart2, ChatResponseReferencePart: extHostTypes.ChatResponseReferencePart, + ChatResponseCodeCitationPart: extHostTypes.ChatResponseCodeCitationPart, ChatResponseWarningPart: extHostTypes.ChatResponseWarningPart, ChatResponseTextEditPart: extHostTypes.ChatResponseTextEditPart, ChatResponseMarkdownWithVulnerabilitiesPart: extHostTypes.ChatResponseMarkdownWithVulnerabilitiesPart, diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index e029c680e42..d1da7affda5 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -194,6 +194,14 @@ class ChatAgentResponseStream { return this; }, + codeCitation(value: vscode.Uri, license: string): void { + throwIfDone(this.codeCitation); + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + + const part = new extHostTypes.ChatResponseCodeCitationPart(value, license); + const dto = typeConvert.ChatResponseCodeCitationPart.from(part); + _report(dto); + }, textEdit(target, edits) { throwIfDone(this.textEdit); checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); @@ -229,7 +237,8 @@ class ChatAgentResponseStream { part instanceof extHostTypes.ChatResponseMarkdownWithVulnerabilitiesPart || part instanceof extHostTypes.ChatResponseDetectedParticipantPart || part instanceof extHostTypes.ChatResponseWarningPart || - part instanceof extHostTypes.ChatResponseConfirmationPart + part instanceof extHostTypes.ChatResponseConfirmationPart || + part instanceof extHostTypes.ChatResponseCodeCitationPart ) { checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 8bf96750570..78f79c87f9c 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -40,7 +40,7 @@ import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from 'vs/workbench/common/edit import { IViewBadge } from 'vs/workbench/common/views'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; import * as chatProvider from 'vs/workbench/contrib/chat/common/languageModels'; import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from 'vs/workbench/contrib/debug/common/debug'; import * as notebooks from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -2540,6 +2540,16 @@ export namespace ChatResponseReferencePart { } } +export namespace ChatResponseCodeCitationPart { + export function from(part: vscode.ChatResponseCodeCitationPart): Dto { + return { + kind: 'codeCitation', + value: part.value, + license: part.license, + }; + } +} + export namespace ChatResponsePart { export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseDetectedParticipantPart | vscode.ChatResponseWarningPart | vscode.ChatResponseConfirmationPart | vscode.ChatResponseWarningPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { @@ -2565,6 +2575,8 @@ export namespace ChatResponsePart { return ChatResponseWarningPart.from(part); } else if (part instanceof types.ChatResponseConfirmationPart) { return ChatResponseConfirmationPart.from(part); + } else if (part instanceof types.ChatResponseCodeCitationPart) { + return ChatResponseCodeCitationPart.from(part); } return { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 2ba4c071688..20c8a208a15 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4473,6 +4473,15 @@ export class ChatResponseReferencePart { } } +export class ChatResponseCodeCitationPart { + value: vscode.Uri; + license: string; + constructor(value: vscode.Uri, license: string) { + this.value = value; + this.license = license; + } +} + export class ChatResponseTextEditPart { uri: vscode.Uri; edits: vscode.TextEdit[]; diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts index b1305e8e160..dca7cf1a23b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts @@ -32,17 +32,23 @@ import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/f const $ = dom.$; -export class ChatReferencesContentPart extends Disposable implements IChatContentPart { +export interface IChatReferenceListItem extends IChatContentReference { + title?: string; +} + +export type IChatCollapsibleListItem = IChatReferenceListItem | IChatWarningMessage; + +export class ChatCollapsibleListContentPart extends Disposable implements IChatContentPart { public readonly domNode: HTMLElement; private readonly _onDidChangeHeight = this._register(new Emitter()); public readonly onDidChangeHeight = this._onDidChangeHeight.event; constructor( - private readonly data: ReadonlyArray, + private readonly data: ReadonlyArray, labelOverride: string | undefined, element: IChatResponseViewModel, - contentReferencesListPool: ContentReferencesListPool, + contentReferencesListPool: CollapsibleListPool, @IOpenerService openerService: IOpenerService, ) { super(); @@ -117,7 +123,8 @@ export class ChatReferencesContentPart extends Disposable implements IChatConten } hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { - return other.kind === 'references' && other.references.length === this.data.length; + return other.kind === 'references' && other.references.length === this.data.length || + other.kind === 'codeCitations' && other.citations.length === this.data.length; } private updateAriaLabel(element: HTMLElement, label: string, expanded?: boolean): void { @@ -129,10 +136,10 @@ export class ChatReferencesContentPart extends Disposable implements IChatConten } } -export class ContentReferencesListPool extends Disposable { - private _pool: ResourcePool>; +export class CollapsibleListPool extends Disposable { + private _pool: ResourcePool>; - public get inUse(): ReadonlySet> { + public get inUse(): ReadonlySet> { return this._pool.inUse; } @@ -145,22 +152,22 @@ export class ContentReferencesListPool extends Disposable { this._pool = this._register(new ResourcePool(() => this.listFactory())); } - private listFactory(): WorkbenchList { + private listFactory(): WorkbenchList { const resourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility })); const container = $('.chat-used-context-list'); this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); const list = this.instantiationService.createInstance( - WorkbenchList, + WorkbenchList, 'ChatListRenderer', container, - new ContentReferencesListDelegate(), - [this.instantiationService.createInstance(ContentReferencesListRenderer, resourceLabels)], + new CollapsibleListDelegate(), + [this.instantiationService.createInstance(CollapsibleListRenderer, resourceLabels)], { alwaysConsumeMouseWheel: false, accessibilityProvider: { - getAriaLabel: (element: IChatContentReference | IChatWarningMessage) => { + getAriaLabel: (element: IChatCollapsibleListItem) => { if (element.kind === 'warning') { return element.content.value; } @@ -174,10 +181,10 @@ export class ContentReferencesListPool extends Disposable { } }, - getWidgetAriaLabel: () => localize('usedReferences', "Used References") + getWidgetAriaLabel: () => localize('chatCollapsibleList', "Collapsible Chat List") }, dnd: { - getDragURI: (element: IChatContentReference | IChatWarningMessage) => { + getDragURI: (element: IChatCollapsibleListItem) => { if (element.kind === 'warning') { return null; } @@ -199,7 +206,7 @@ export class ContentReferencesListPool extends Disposable { return list; } - get(): IDisposableReference> { + get(): IDisposableReference> { const object = this._pool.get(); let stale = false; return { @@ -213,24 +220,24 @@ export class ContentReferencesListPool extends Disposable { } } -class ContentReferencesListDelegate implements IListVirtualDelegate { - getHeight(element: IChatContentReference): number { +class CollapsibleListDelegate implements IListVirtualDelegate { + getHeight(element: IChatCollapsibleListItem): number { return 22; } - getTemplateId(element: IChatContentReference): string { - return ContentReferencesListRenderer.TEMPLATE_ID; + getTemplateId(element: IChatCollapsibleListItem): string { + return CollapsibleListRenderer.TEMPLATE_ID; } } -interface IChatContentReferenceListTemplate { +interface ICollapsibleListTemplate { label: IResourceLabel; templateDisposables: IDisposable; } -class ContentReferencesListRenderer implements IListRenderer { - static TEMPLATE_ID = 'contentReferencesListRenderer'; - readonly templateId: string = ContentReferencesListRenderer.TEMPLATE_ID; +class CollapsibleListRenderer implements IListRenderer { + static TEMPLATE_ID = 'chatCollapsibleListRenderer'; + readonly templateId: string = CollapsibleListRenderer.TEMPLATE_ID; constructor( private labels: ResourceLabels, @@ -238,7 +245,7 @@ class ContentReferencesListRenderer implements IListRenderer { + this.updateItemHeight(templateData); + })); + + return referencesPart; + } + + private renderCodeCitationsListData(citations: IChatCodeCitations, context: IChatContentPartRenderContext, templateData: IChatListItemTemplate): ChatCollapsibleListContentPart { + const citationsAsReferences: IChatCollapsibleListItem[] = citations.citations.map(citation => { + return { + kind: 'reference', + reference: citation.value, + title: `License: ${citation.license}\n${citation.value}` + } satisfies IChatCollapsibleListItem; + }); + + const label = citationsAsReferences.length > 1 ? + localize('codeCitationsPlural', "This code matches {0} references in public repositories", citationsAsReferences.length) : + localize('codeCitation', "This code matches 1 reference in public repositories", citationsAsReferences.length); + const referencesPart = this.instantiationService.createInstance(ChatCollapsibleListContentPart, citationsAsReferences, label, context.element as IChatResponseViewModel, this._contentReferencesListPool); referencesPart.addDisposable(referencesPart.onDidChangeHeight(() => { this.updateItemHeight(templateData); })); diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 113f94f6c8a..32c7703581c 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -21,7 +21,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ILogService } from 'vs/platform/log/common/log'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService, reviveSerializedAgent } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestTextPart, IParsedChatRequest, getPromptText, reviveParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, ChatAgentVoteDirection, isIUsedContext, IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, ChatAgentVoteDirection, isIUsedContext, IChatProgress, IChatCodeCitation } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; export interface IChatRequestVariableEntry { @@ -94,6 +94,7 @@ export interface IChatResponseModel { readonly agent?: IChatAgentData; readonly usedContext: IChatUsedContext | undefined; readonly contentReferences: ReadonlyArray; + readonly codeCitations: ReadonlyArray; readonly progressMessages: ReadonlyArray; readonly slashCommand?: IChatAgentCommand; readonly agentOrSlashCommandDetected: boolean; @@ -365,6 +366,11 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel return this._contentReferences; } + private readonly _codeCitations: IChatCodeCitation[] = []; + public get codeCitations(): ReadonlyArray { + return this._codeCitations; + } + private readonly _progressMessages: IChatProgressMessage[] = []; public get progressMessages(): ReadonlyArray { return this._progressMessages; @@ -417,6 +423,11 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel } } + applyCodeCitation(progress: IChatCodeCitation) { + this._codeCitations.push(progress); + this._onDidChange.fire(); + } + setAgent(agent: IChatAgentData, slashCommand?: IChatAgentCommand) { this._agent = agent; this._slashCommand = slashCommand; @@ -508,6 +519,7 @@ export interface ISerializableChatRequestData { /** For backward compat: should be optional */ usedContext?: IChatUsedContext; contentReferences?: ReadonlyArray; + codeCitations?: ReadonlyArray; } export interface IExportableChatData { @@ -747,9 +759,8 @@ export class ChatModel extends Disposable implements IChatModel { request.response.applyReference(revive(raw.usedContext)); } - if (raw.contentReferences) { - raw.contentReferences.forEach(r => request.response!.applyReference(revive(r))); - } + raw.contentReferences?.forEach(r => request.response!.applyReference(revive(r))); + raw.codeCitations?.forEach(c => request.response!.applyCodeCitation(revive(c))); } return request; }); @@ -901,6 +912,8 @@ export class ChatModel extends Disposable implements IChatModel { request.response.setAgent(agent, progress.command); this._onDidChange.fire({ kind: 'setAgent', agent, command: progress.command }); } + } else if (progress.kind === 'codeCitation') { + request.response.applyCodeCitation(progress); } else { this.logService.error(`Couldn't handle progress: ${JSON.stringify(progress)}`); } @@ -994,7 +1007,8 @@ export class ChatModel extends Disposable implements IChatModel { agent: r.response?.agent ? { ...r.response.agent } : undefined, slashCommand: r.response?.slashCommand, usedContext: r.response?.usedContext, - contentReferences: r.response?.contentReferences + contentReferences: r.response?.contentReferences, + codeCitations: r.response?.codeCitations }; }), }; diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 645ba778d36..93ff057fa04 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -81,6 +81,12 @@ export interface IChatContentReference { kind: 'reference'; } +export interface IChatCodeCitation { + value: URI; + license: string; + kind: 'codeCitation'; +} + export interface IChatContentInlineReference { inlineReference: URI | Location; name?: string; @@ -172,6 +178,7 @@ export type IChatProgress = | IChatUsedContext | IChatContentReference | IChatContentInlineReference + | IChatCodeCitation | IChatAgentDetection | IChatProgressMessage | IChatTask diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 647809f1721..e05c045520a 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -15,7 +15,7 @@ import { annotateVulnerabilitiesInText } from 'vs/workbench/contrib/chat/common/ import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModelInitState, IChatModel, IChatProgressRenderableResponseContent, IChatRequestModel, IChatRequestVariableEntry, IChatResponseModel, IChatTextEditGroup, IChatWelcomeMessageContent, IResponse } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { ChatAgentVoteDirection, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatTask, IChatUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatAgentVoteDirection, IChatCodeCitation, IChatContentReference, IChatFollowup, IChatProgressMessage, IChatResponseErrorDetails, IChatTask, IChatUsedContext } from 'vs/workbench/contrib/chat/common/chatService'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { CodeBlockModelCollection } from './codeBlockModelCollection'; import { hash } from 'vs/base/common/hash'; @@ -125,10 +125,18 @@ export interface IChatReferences { kind: 'references'; } +/** + * Content type for citations used during rendering, not in the model + */ +export interface IChatCodeCitations { + citations: ReadonlyArray; + kind: 'codeCitations'; +} + /** * Type for content parts rendered by IChatListRenderer */ -export type IChatRendererContent = IChatProgressRenderableResponseContent | IChatReferences; +export type IChatRendererContent = IChatProgressRenderableResponseContent | IChatReferences | IChatCodeCitations; export interface IChatLiveUpdateData { firstWordTime: number; @@ -153,6 +161,7 @@ export interface IChatResponseViewModel { readonly response: IResponse; readonly usedContext: IChatUsedContext | undefined; readonly contentReferences: ReadonlyArray; + readonly codeCitations: ReadonlyArray; readonly progressMessages: ReadonlyArray; readonly isComplete: boolean; readonly isCanceled: boolean; @@ -437,6 +446,10 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi return this._model.contentReferences; } + get codeCitations(): ReadonlyArray { + return this._model.codeCitations; + } + get progressMessages(): ReadonlyArray { return this._model.progressMessages; } diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap index 78fe7781692..9b85a97b4d7 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap @@ -91,7 +91,8 @@ ], kind: "usedContext" }, - contentReferences: [ ] + contentReferences: [ ], + codeCitations: [ ] } ] } \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap index be8bb0fed92..421835ade92 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap @@ -91,7 +91,8 @@ ], kind: "usedContext" }, - contentReferences: [ ] + contentReferences: [ ], + codeCitations: [ ] } ] } \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap index a749780f183..d0b833b4c19 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap @@ -75,7 +75,8 @@ }, slashCommand: undefined, usedContext: undefined, - contentReferences: [ ] + contentReferences: [ ], + codeCitations: [ ] } ] } \ No newline at end of file diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index ab43814c4f4..8911336b3df 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -63,7 +63,13 @@ declare module 'vscode' { constructor(title: string, message: string, data: any, buttons?: string[]); } - export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseDetectedParticipantPart | ChatResponseConfirmationPart; + export class ChatResponseCodeCitationPart { + value: Uri; + license: string; + constructor(value: Uri, license: string); + } + + export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseDetectedParticipantPart | ChatResponseConfirmationPart | ChatResponseCodeCitationPart; export class ChatResponseWarningPart { value: MarkdownString; @@ -116,6 +122,8 @@ declare module 'vscode' { reference(value: Uri | Location | { variableName: string; value?: Uri | Location }, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }): void; + codeCitation(value: Uri, license: string): void; + push(part: ExtendedChatResponsePart): void; }