diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 85c7ba9122a..865436abcf7 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1763,6 +1763,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseProgressPart: extHostTypes.ChatResponseProgressPart, ChatResponseProgressPart2: extHostTypes.ChatResponseProgressPart2, ChatResponseReferencePart: extHostTypes.ChatResponseReferencePart, + ChatResponseReferencePart2: extHostTypes.ChatResponseReferencePart, ChatResponseCodeCitationPart: extHostTypes.ChatResponseCodeCitationPart, ChatResponseWarningPart: extHostTypes.ChatResponseWarningPart, ChatResponseTextEditPart: extHostTypes.ChatResponseTextEditPart, @@ -1770,6 +1771,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseCommandButtonPart: extHostTypes.ChatResponseCommandButtonPart, ChatResponseDetectedParticipantPart: extHostTypes.ChatResponseDetectedParticipantPart, ChatResponseConfirmationPart: extHostTypes.ChatResponseConfirmationPart, + ChatResponseReferencePartStatusKind: extHostTypes.ChatResponseReferencePartStatusKind, ChatRequestTurn: extHostTypes.ChatRequestTurn, ChatResponseTurn: extHostTypes.ChatResponseTurn, ChatLocation: extHostTypes.ChatLocation, diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 490c0719be2..46a2a57283f 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -158,6 +158,9 @@ class ChatAgentResponseStream { return this; }, reference(value, iconPath) { + return this.reference2(value, iconPath); + }, + reference2(value, iconPath, options) { throwIfDone(this.reference); if ('variableName' in value) { @@ -176,7 +179,7 @@ class ChatAgentResponseStream { } satisfies IChatContentReference)); } else { // Participant sent a variableName reference but the variable produced no references. Show variable reference with no value - const part = new extHostTypes.ChatResponseReferencePart(value, iconPath); + const part = new extHostTypes.ChatResponseReferencePart(value, iconPath, options); const dto = typeConvert.ChatResponseReferencePart.from(part); references = [dto]; } @@ -187,7 +190,7 @@ class ChatAgentResponseStream { // Something went wrong- that variable doesn't actually exist } } else { - const part = new extHostTypes.ChatResponseReferencePart(value, iconPath); + const part = new extHostTypes.ChatResponseReferencePart(value, iconPath, options); const dto = typeConvert.ChatResponseReferencePart.from(part); _report(dto); } @@ -245,7 +248,7 @@ class ChatAgentResponseStream { if (part instanceof extHostTypes.ChatResponseReferencePart) { // Ensure variable reference values get fixed up - this.reference(part.value, part.iconPath); + this.reference2(part.value, part.iconPath, part.options); } else { const dto = typeConvert.ChatResponsePart.from(part, that._commandsConverter, that._sessionDisposables); _report(dto); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 459ee6b05f9..3d0dba250b4 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2511,7 +2511,8 @@ export namespace ChatResponseReferencePart { part.value.value : Location.from(part.value.value as vscode.Location) }, - iconPath + iconPath, + options: part.options }; } @@ -2520,7 +2521,8 @@ export namespace ChatResponseReferencePart { reference: URI.isUri(part.value) ? part.value : Location.from(part.value), - iconPath + iconPath, + options: part.options }; } export function to(part: Dto): vscode.ChatResponseReferencePart { @@ -2553,7 +2555,7 @@ export namespace ChatResponseCodeCitationPart { 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 { + export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseDetectedParticipantPart | vscode.ChatResponseWarningPart | vscode.ChatResponseConfirmationPart | vscode.ChatResponseReferencePart2, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { if (part instanceof types.ChatResponseMarkdownPart) { return ChatResponseMarkdownPart.from(part); } else if (part instanceof types.ChatResponseAnchorPart) { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 7f5f6f92b25..b0167a87039 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4467,9 +4467,11 @@ export class ChatResponseCommandButtonPart { export class ChatResponseReferencePart { value: vscode.Uri | vscode.Location | { variableName: string; value?: vscode.Uri | vscode.Location }; iconPath?: vscode.Uri | vscode.ThemeIcon | { light: vscode.Uri; dark: vscode.Uri }; - constructor(value: vscode.Uri | vscode.Location | { variableName: string; value?: vscode.Uri | vscode.Location }, iconPath?: vscode.Uri | vscode.ThemeIcon | { light: vscode.Uri; dark: vscode.Uri }) { + options?: { status?: { description: string; kind: vscode.ChatResponseReferencePartStatusKind } }; + constructor(value: vscode.Uri | vscode.Location | { variableName: string; value?: vscode.Uri | vscode.Location }, iconPath?: vscode.Uri | vscode.ThemeIcon | { light: vscode.Uri; dark: vscode.Uri }, options?: { status?: { description: string; kind: vscode.ChatResponseReferencePartStatusKind } }) { this.value = value; this.iconPath = iconPath; + this.options = options; } } @@ -4519,6 +4521,12 @@ export enum ChatLocation { Editor = 4, } +export enum ChatResponseReferencePartStatusKind { + Complete = 1, + Partial = 2, + Omitted = 3 +} + export class ChatRequestEditorData implements vscode.ChatRequestEditorData { constructor( readonly document: vscode.TextDocument, diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts index ebcc16d2ca5..1d882b5a67e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -14,6 +14,8 @@ import { FileKind } from 'vs/platform/files/common/files'; import { Range } from 'vs/editor/common/core/range'; import { basename, dirname } from 'vs/base/common/path'; import { localize } from 'vs/nls'; +import { ChatResponseReferencePartStatusKind, IChatContentReference } from 'vs/workbench/contrib/chat/common/chatService'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; export class ChatAttachmentsContentPart extends Disposable { private readonly attachedContextDisposables = this._register(new DisposableStore()); @@ -23,8 +25,10 @@ export class ChatAttachmentsContentPart extends Disposable { constructor( private readonly variables: IChatRequestVariableEntry[], + private readonly contentReferences: ReadonlyArray = [], public readonly domNode: HTMLElement = dom.$('.chat-attached-context'), @IInstantiationService private readonly instantiationService: IInstantiationService, + @IOpenerService private readonly openerService: IOpenerService, ) { super(); @@ -36,32 +40,65 @@ export class ChatAttachmentsContentPart extends Disposable { this.attachedContextDisposables.clear(); dom.setVisibility(Boolean(this.variables.length), this.domNode); - this.variables.forEach((attachment, index) => { + this.variables.forEach((attachment) => { const widget = dom.append(container, dom.$('.chat-attached-context-attachment.show-file-icons')); const label = this._contextResourceLabels.create(widget, { supportIcons: true }); const file = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined; const range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined; + const correspondingContentReference = this.contentReferences.find((ref) => 'variableName' in ref.reference && ref.reference.variableName === attachment.name); + if (file) { const fileBasename = basename(file.path); const fileDirname = dirname(file.path); const friendlyName = `${fileBasename} ${fileDirname}`; - const ariaLabel = range ? localize('chat.fileAttachmentWithRange', "Attached file, {0}, line {1} to line {2}", friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.fileAttachment', "Attached file, {0}", friendlyName); + const ariaLabel = range ? localize('chat.fileAttachmentWithRange2', "Attached file, {0}, line {1} to line {2}.", friendlyName, range.startLineNumber, range.endLineNumber) : localize('chat.fileAttachment2', "Attached file, {0}.", friendlyName); label.setFile(file, { fileKind: FileKind.FILE, hidePath: true, range, + title: correspondingContentReference?.options?.status?.description }); widget.ariaLabel = ariaLabel; widget.tabIndex = 0; + widget.style.cursor = 'pointer'; + + this.attachedContextDisposables.add(dom.addDisposableListener(widget, dom.EventType.CLICK, async (e: MouseEvent) => { + dom.EventHelper.stop(e, true); + if (file) { + this.openerService.open( + file, + { + fromUserGesture: true, + editorOptions: { + selection: range + } as any + }); + } + })); } else { const attachmentLabel = attachment.fullName ?? attachment.name; - label.setLabel(attachmentLabel, undefined); + label.setLabel(attachmentLabel, correspondingContentReference?.options?.status?.description); - widget.ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name); + widget.ariaLabel = localize('chat.attachment2', "Attached context, {0}.", attachment.name); widget.tabIndex = 0; } + + const isAttachmentPartialOrOmitted = correspondingContentReference?.options?.status?.kind === ChatResponseReferencePartStatusKind.Omitted || correspondingContentReference?.options?.status?.kind === ChatResponseReferencePartStatusKind.Partial; + if (isAttachmentPartialOrOmitted) { + widget.classList.add('warning'); + } + const description = correspondingContentReference?.options?.status?.description; + if (isAttachmentPartialOrOmitted) { + widget.ariaLabel = `${widget.ariaLabel}${description ? ` ${description}` : ''}`; + for (const selector of ['.monaco-icon-suffix-container', '.monaco-icon-name-container']) { + const element = label.element.querySelector(selector); + if (element) { + element.classList.add('warning'); + } + } + } }); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts index 1ce49d6f21a..91df37d7f0e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts @@ -26,7 +26,7 @@ import { ColorScheme } from 'vs/workbench/browser/web.api'; import { ChatTreeItem } from 'vs/workbench/contrib/chat/browser/chat'; import { IDisposableReference, ResourcePool } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatCollections'; import { IChatContentPart } from 'vs/workbench/contrib/chat/browser/chatContentParts/chatContentParts'; -import { IChatContentReference, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatResponseReferencePartStatusKind, IChatContentReference, IChatWarningMessage } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatRendererContent, IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/files/browser/views/explorerView'; @@ -283,10 +283,10 @@ class CollapsibleListRenderer implements IListRenderer | undefined, templateData: IChatListItemTemplate) { + return this.instantiationService.createInstance(ChatAttachmentsContentPart, variables, contentReferences, undefined); } private renderTextEdit(context: IChatContentPartRenderContext, chatTextEdit: IChatTextEditGroup, templateData: IChatListItemTemplate): IChatContentPart { diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 692eb2c379c..0549960bbb1 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -482,6 +482,17 @@ color: var(--vscode-interactive-session-foreground); } +.chat-attached-context .chat-attached-context-attachment .monaco-icon-name-container.warning, +.chat-attached-context .chat-attached-context-attachment .monaco-icon-suffix-container.warning, +.chat-used-context-list .monaco-icon-name-container.warning, +.chat-used-context-list .monaco-icon-suffix-container.warning { + color: var(--vscode-notificationsWarningIcon-foreground); +} + +.chat-attached-context .chat-attached-context-attachment.show-file-icons.warning { + border-color: var(--vscode-notificationsWarningIcon-foreground); +} + .chat-notification-widget .chat-warning-codicon .codicon-warning { color: var(--vscode-notificationsWarningIcon-foreground) !important; /* Have to override default styles which apply to all lists */ } diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 82ef9c5da66..6c9be3f767f 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -75,9 +75,16 @@ export interface IChatContentVariableReference { value?: URI | Location; } +export enum ChatResponseReferencePartStatusKind { + Complete = 1, + Partial = 2, + Omitted = 3 +} + export interface IChatContentReference { reference: URI | Location | IChatContentVariableReference; iconPath?: ThemeIcon | { light: URI; dark?: URI }; + options?: { status?: { description: string; kind: ChatResponseReferencePartStatusKind } }; kind: 'reference'; } diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index e05c045520a..fc34fce7ec3 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -71,6 +71,7 @@ export interface IChatRequestViewModel { readonly attempt: number; readonly variables: IChatRequestVariableEntry[]; currentRenderedHeight: number | undefined; + readonly contentReferences?: ReadonlyArray; } export interface IChatResponseMarkdownRenderData { @@ -376,6 +377,10 @@ export class ChatRequestViewModel implements IChatRequestViewModel { return this._model.variableData.variables; } + get contentReferences() { + return this._model.response?.contentReferences; + } + currentRenderedHeight: number | undefined; constructor( diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 6910027ec32..a0b49942433 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -70,7 +70,7 @@ declare module 'vscode' { constructor(value: Uri, license: string, snippet: string); } - export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseDetectedParticipantPart | ChatResponseConfirmationPart | ChatResponseCodeCitationPart; + export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseDetectedParticipantPart | ChatResponseConfirmationPart | ChatResponseCodeCitationPart | ChatResponseReferencePart2; export class ChatResponseWarningPart { value: MarkdownString; @@ -83,6 +83,44 @@ declare module 'vscode' { constructor(value: string, task?: (progress: Progress) => Thenable); } + export class ChatResponseReferencePart2 { + /** + * The reference target. + */ + value: Uri | Location | { variableName: string; value?: Uri | Location }; + + /** + * The icon for the reference. + */ + iconPath?: Uri | ThemeIcon | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + }; + options?: { status?: { description: string; kind: ChatResponseReferencePartStatusKind } }; + + /** + * Create a new ChatResponseReferencePart. + * @param value A uri or location + * @param iconPath Icon for the reference shown in UI + */ + constructor(value: Uri | Location | { variableName: string; value?: Uri | Location }, iconPath?: Uri | ThemeIcon | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + }, options?: { status?: { description: string; kind: ChatResponseReferencePartStatusKind } }); + } + export interface ChatResponseStream { /** @@ -123,11 +161,19 @@ declare module 'vscode' { reference(value: Uri | Location | { variableName: string; value?: Uri | Location }, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }): void; + reference2(value: Uri | Location | { variableName: string; value?: Uri | Location }, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }, options?: { status?: { description: string; kind: ChatResponseReferencePartStatusKind } }): void; + codeCitation(value: Uri, license: string, snippet: string): void; push(part: ExtendedChatResponsePart): void; } + export enum ChatResponseReferencePartStatusKind { + Complete = 1, + Partial = 2, + Omitted = 3 + } + /** * Does this piggy-back on the existing ChatRequest, or is it a different type of request entirely? * Does it show up in history?