diff --git a/src/vs/sessions/contrib/chat/browser/agentHostInputCompletions.ts b/src/vs/sessions/contrib/chat/browser/agentHostInputCompletions.ts index d4764a6651d040..947408aadfe252 100644 --- a/src/vs/sessions/contrib/chat/browser/agentHostInputCompletions.ts +++ b/src/vs/sessions/contrib/chat/browser/agentHostInputCompletions.ts @@ -17,7 +17,7 @@ import { CompletionItem, CompletionItemKind } from '../../../../editor/common/la import { ITextModel } from '../../../../editor/common/model.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; -import { AgentHostCompletionReferenceKind, agentHostCompletionVariableValue, IChatRequestVariableEntry, isAgentHostCompletionVariableEntry } from '../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js'; +import { AgentHostCompletionReferenceKind, IChatRequestVariableEntry, isAgentHostCompletionVariableEntry, toAgentHostCompletionVariableEntry } from '../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js'; import { IChatInputCompletionItem, IChatSessionsService, isAgentHostTarget } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; import { getChatSessionType } from '../../../../workbench/contrib/chat/common/model/chatUri.js'; import { chatSlashCommandBackground, chatSlashCommandForeground } from '../../../../workbench/contrib/chat/common/widget/chatColors.js'; @@ -199,13 +199,7 @@ export class AgentHostInputCompletionHandler extends AgentHostInputCompletionsBa switch (attachment.kind) { case 'command': { const referenceText = item.insertText.trimEnd(); - const entry: IChatRequestVariableEntry = { - id: 'agent-host-command:' + attachment.command, - name: referenceText, - value: agentHostCompletionVariableValue(AgentHostCompletionReferenceKind.Command), - kind: 'generic', - _meta: attachment._meta, - }; + const entry = toAgentHostCompletionVariableEntry(AgentHostCompletionReferenceKind.Command, referenceText, attachment.command, attachment._meta); return { label: item.insertText, insertText: item.insertText, @@ -227,13 +221,7 @@ export class AgentHostInputCompletionHandler extends AgentHostInputCompletionsBa } case 'skill': { const referenceText = item.insertText.trimEnd(); - const entry: IChatRequestVariableEntry = { - id: attachment.uri.toString(), - name: referenceText, - value: agentHostCompletionVariableValue(AgentHostCompletionReferenceKind.Skill), - kind: 'generic', - _meta: attachment._meta, - }; + const entry = toAgentHostCompletionVariableEntry(AgentHostCompletionReferenceKind.Skill, referenceText, attachment.uri, attachment._meta); return { label: { label: item.insertText, description: attachment.description }, insertText: item.insertText, diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/stateToProgressAdapter.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/stateToProgressAdapter.ts index f4fbd2f63afb6d..254e09310fb9cc 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/stateToProgressAdapter.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/stateToProgressAdapter.ts @@ -17,7 +17,7 @@ import { type ChatExternalEditKind, type IChatExternalEdit, type IChatModifiedFi import { type IChatSessionHistoryItem } from '../../../common/chatSessionsService.js'; import { ChatToolInvocation } from '../../../common/model/chatProgressTypes/chatToolInvocation.js'; import { type IChatRequestVariableData } from '../../../common/model/chatModel.js'; -import type { IChatRequestVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; +import { AgentHostCompletionReferenceKind, toAgentHostCompletionVariableEntryFromMetadata, type IChatRequestVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; import { type IToolConfirmationMessages, type IToolData, type IToolResult, type IToolResultInputOutputDetails, ToolDataSource, ToolInvocationPresentation } from '../../../common/tools/languageModelToolsService.js'; import { basename, isEqual } from '../../../../../../base/common/resources.js'; import { hasKey } from '../../../../../../base/common/types.js'; @@ -320,6 +320,11 @@ function messageAttachmentToVariableEntry(attachment: MessageAttachment, connect }; } + const agentHostCompletionKind = getAgentHostCompletionKind(attachment); + if (agentHostCompletionKind !== undefined) { + return toAgentHostCompletionVariableEntryFromMetadata(agentHostCompletionKind, attachment.label, attachment._meta); + } + const modelRepresentation = attachment.type === MessageAttachmentKind.Simple ? attachment.modelRepresentation : undefined; return { kind: 'generic', @@ -330,6 +335,19 @@ function messageAttachmentToVariableEntry(attachment: MessageAttachment, connect }; } +function getAgentHostCompletionKind(attachment: MessageAttachment): AgentHostCompletionReferenceKind | undefined { + if (attachment.type !== MessageAttachmentKind.Simple) { + return undefined; + } + switch (attachment.displayKind) { + case 'command': + return AgentHostCompletionReferenceKind.Command; + case 'skill': + return AgentHostCompletionReferenceKind.Skill; + } + return undefined; +} + function textRangeToIRange(range: TextRange): IRange { return { startLineNumber: range.start.line + 1, diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAttachmentsContentPart.ts index 4258a7df2f43c3..9331a82aa9d62e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAttachmentsContentPart.ts @@ -11,7 +11,7 @@ import { URI } from '../../../../../../base/common/uri.js'; import { Range } from '../../../../../../editor/common/core/range.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ResourceLabels } from '../../../../../browser/labels.js'; -import { getImageAttachmentLimit, IChatRequestVariableEntry, isBrowserViewVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isTerminalVariableEntry, isWorkspaceVariableEntry, OmittedState } from '../../../common/attachments/chatVariableEntries.js'; +import { getImageAttachmentLimit, IChatRequestVariableEntry, isAgentHostCompletionVariableEntry, isBrowserViewVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isTerminalVariableEntry, isWorkspaceVariableEntry, OmittedState } from '../../../common/attachments/chatVariableEntries.js'; import { ChatResponseReferencePartStatusKind, IChatContentReference } from '../../../common/chatService/chatService.js'; import { ILanguageModelsService } from '../../../common/languageModels.js'; import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, BrowserViewAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, PromptTextAttachmentWidget, SCMHistoryItemAttachmentWidget, SCMHistoryItemChangeAttachmentWidget, SCMHistoryItemChangeRangeAttachmentWidget, TerminalCommandAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from '../../attachments/chatAttachmentWidgets.js'; @@ -75,8 +75,10 @@ export class ChatAttachmentsContentPart extends Disposable { dom.clearNode(container); this.attachedContextDisposables.clear(); - const visibleAttachments = this.getVisibleAttachments(); - const hasMoreAttachments = this.limit && this._variables.length > this.limit && !this._showingAll; + const renderableAttachments = this.getRenderableAttachments(); + const visibleAttachments = this.getVisibleAttachments(renderableAttachments); + const remainingCount = renderableAttachments.length - visibleAttachments.length; + const hasMoreAttachments = remainingCount > 0 && !this._showingAll; this.markImageLimitExceeded(this._variables); @@ -85,15 +87,19 @@ export class ChatAttachmentsContentPart extends Disposable { } if (hasMoreAttachments) { - this.renderShowMoreButton(container); + this.renderShowMoreButton(container, remainingCount); } } - private getVisibleAttachments(): readonly IChatRequestVariableEntry[] { + private getRenderableAttachments(): readonly IChatRequestVariableEntry[] { + return this._variables.filter(attachment => !isAgentHostCompletionVariableEntry(attachment)); + } + + private getVisibleAttachments(visibleAttachments: readonly IChatRequestVariableEntry[]): readonly IChatRequestVariableEntry[] { if (!this.limit || this._showingAll) { - return this._variables; + return visibleAttachments; } - return this._variables.slice(0, this.limit); + return visibleAttachments.slice(0, this.limit); } /** @@ -145,9 +151,7 @@ export class ChatAttachmentsContentPart extends Disposable { return getImageAttachmentLimit(this.languageModelsService.lookupLanguageModel(this.modelId)); } - private renderShowMoreButton(container: HTMLElement) { - const remainingCount = this._variables.length - (this.limit ?? 0); - + private renderShowMoreButton(container: HTMLElement, remainingCount: number) { // Create a button that looks like the attachment pills const showMoreButton = dom.$('div.chat-attached-context-attachment.chat-attachments-show-more-button'); showMoreButton.setAttribute('role', 'button'); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/editor/agentHostInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/agentHostInputCompletions.ts index 5be43d63ba23c9..8a268d479faabd 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/editor/agentHostInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/agentHostInputCompletions.ts @@ -7,7 +7,7 @@ import { DisposableMap } from '../../../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../../../base/common/network.js'; import { assertType } from '../../../../../../../base/common/types.js'; import { URI } from '../../../../../../../base/common/uri.js'; -import { AgentHostCompletionReferenceKind, agentHostCompletionVariableValue, type IAgentHostCompletionVariableValue } from '../../../../common/attachments/chatVariableEntries.js'; +import { AgentHostCompletionReferenceKind, toAgentHostCompletionVariableEntry, type IAgentHostCompletionVariableValue } from '../../../../common/attachments/chatVariableEntries.js'; import { Position } from '../../../../../../../editor/common/core/position.js'; import { Range } from '../../../../../../../editor/common/core/range.js'; import { CompletionItem, CompletionItemKind } from '../../../../../../../editor/common/languages.js'; @@ -199,11 +199,13 @@ class AgentHostReferenceArgument { } static forSkill(widget: IChatWidget, uri: URI, displayName: string | undefined, range: Range, _meta: Record | undefined): AgentHostReferenceArgument { - return new AgentHostReferenceArgument(widget, uri.toString(), agentHostCompletionVariableValue(AgentHostCompletionReferenceKind.Skill), displayName, false, false, range, _meta); + const entry = toAgentHostCompletionVariableEntry(AgentHostCompletionReferenceKind.Skill, displayName ?? uri.toString(), uri, _meta); + return new AgentHostReferenceArgument(widget, entry.id, entry.value, displayName, false, false, range, _meta); } static forCommand(widget: IChatWidget, command: string, description: string | undefined, range: Range, _meta: Record | undefined): AgentHostReferenceArgument { - return new AgentHostReferenceArgument(widget, 'agent-host-command:' + command, agentHostCompletionVariableValue(AgentHostCompletionReferenceKind.Command), description, false, false, range, _meta); + const entry = toAgentHostCompletionVariableEntry(AgentHostCompletionReferenceKind.Command, description ?? command, command, _meta); + return new AgentHostReferenceArgument(widget, entry.id, entry.value, description, false, false, range, _meta); } } diff --git a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts index 1922ac7de80aed..6695411a2561ab 100644 --- a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts +++ b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts @@ -8,6 +8,7 @@ import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; import { basename } from '../../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; +import { generateUuid } from '../../../../../base/common/uuid.js'; import { IRange } from '../../../../../editor/common/core/range.js'; import { IOffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js'; import { isLocation, Location, SymbolKind } from '../../../../../editor/common/languages.js'; @@ -63,10 +64,38 @@ export interface IAgentHostCompletionVariableValue { readonly kind: AgentHostCompletionReferenceKind; } -export function agentHostCompletionVariableValue(kind: AgentHostCompletionReferenceKind): IAgentHostCompletionVariableValue { +function agentHostCompletionVariableValue(kind: AgentHostCompletionReferenceKind): IAgentHostCompletionVariableValue { return { $mid: 'agentHostCompletion', kind }; } +function agentHostCompletionVariableId(kind: AgentHostCompletionReferenceKind, reference: URI | string): string { + switch (kind) { + case AgentHostCompletionReferenceKind.Skill: + return reference.toString(); + case AgentHostCompletionReferenceKind.Command: + return 'agent-host-command:' + reference.toString(); + } +} + +export function toAgentHostCompletionVariableEntry(kind: AgentHostCompletionReferenceKind, name: string, reference: URI | string | undefined, _meta: Record | undefined): IGenericChatRequestVariableEntry & { value: IAgentHostCompletionVariableValue } { + return { + kind: 'generic', + id: reference !== undefined ? agentHostCompletionVariableId(kind, reference) : generateUuid(), + name, + value: agentHostCompletionVariableValue(kind), + _meta, + }; +} + +export function toAgentHostCompletionVariableEntryFromMetadata(kind: AgentHostCompletionReferenceKind, name: string, _meta: Record | undefined): IGenericChatRequestVariableEntry & { value: IAgentHostCompletionVariableValue } { + switch (kind) { + case AgentHostCompletionReferenceKind.Skill: + return toAgentHostCompletionVariableEntry(kind, name, typeof _meta?.uri === 'string' ? _meta.uri : undefined, _meta); + case AgentHostCompletionReferenceKind.Command: + return toAgentHostCompletionVariableEntry(kind, name, typeof _meta?.command === 'string' ? _meta.command : undefined, _meta); + } +} + export function getAgentHostCompletionReferenceKind(entry: IChatRequestVariableEntry): AgentHostCompletionReferenceKind | undefined { if (entry.kind !== 'generic' || typeof entry.value !== 'object' || entry.value === null) { return undefined; diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts index 3c9171cd6e6d14..2635747a49be94 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostChatContribution.test.ts @@ -69,7 +69,7 @@ import { ChatQuestionCarouselData } from '../../../common/model/chatProgressType import { ChatElicitationRequestPart } from '../../../common/model/chatProgressTypes/chatElicitationRequestPart.js'; import type { IChatModel, IChatPendingRequest, IChatRequestModel } from '../../../common/model/chatModel.js'; import { convertBufferToScreenshotVariable } from '../../../browser/attachments/chatScreenshotContext.js'; -import { AgentHostCompletionReferenceKind, agentHostCompletionVariableValue } from '../../../common/attachments/chatVariableEntries.js'; +import { AgentHostCompletionReferenceKind, toAgentHostCompletionVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; // ---- Mock agent host service ------------------------------------------------ @@ -773,20 +773,12 @@ suite('AgentHostChatContribution', () => { variables: { variables: [ { - kind: 'generic', - id: 'file:///skills/author-contributions/SKILL.md', - name: '/author-contributions', - value: agentHostCompletionVariableValue(AgentHostCompletionReferenceKind.Skill), + ...toAgentHostCompletionVariableEntry(AgentHostCompletionReferenceKind.Skill, '/author-contributions', 'file:///skills/author-contributions/SKILL.md', skillMeta), range: { start: skillStart, endExclusive: skillStart + '/author-contributions'.length }, - _meta: skillMeta, }, { - kind: 'generic', - id: 'agent-host-command:rename', - name: '/rename', - value: agentHostCompletionVariableValue(AgentHostCompletionReferenceKind.Command), + ...toAgentHostCompletionVariableEntry(AgentHostCompletionReferenceKind.Command, '/rename', 'rename', commandMeta), range: { start: commandStart, endExclusive: commandStart + '/rename'.length }, - _meta: commandMeta, }, ], }, @@ -2552,6 +2544,68 @@ suite('AgentHostChatContribution', () => { } }); + test('restores agent host completion attachments as hidden request variables', async () => { + const { sessionHandler, agentHostService } = createContribution(disposables); + const sessionResource = URI.from({ scheme: 'agent-host-copilot', path: '/completion-history' }); + const sessionUri = AgentSession.uri('copilot', 'completion-history'); + const skillMeta = { + uri: 'file:///skills/agent-host-docs/SKILL.md', + displayName: 'agent-host-docs', + description: 'Use this skill when working on Agent Host code', + }; + const commandMeta = { + command: 'rename', + description: 'Rename this chat', + }; + + agentHostService.sessionStates.set(sessionUri.toString(), { + ...createSessionState({ resource: sessionUri.toString(), provider: 'copilot', title: 'Test', status: SessionStatus.Idle, createdAt: Date.now(), modifiedAt: Date.now() }), + lifecycle: SessionLifecycle.Ready, + turns: [{ + id: 'turn-1', + message: { + text: '/agent-host-docs please check this\n/rename Title', + origin: { kind: MessageKind.User }, + attachments: [ + { + type: MessageAttachmentKind.Simple, + label: '/agent-host-docs', + displayKind: 'skill', + _meta: skillMeta, + }, + { + type: MessageAttachmentKind.Simple, + label: '/rename', + displayKind: 'command', + _meta: commandMeta, + }, + ], + }, + responseParts: [], + usage: undefined, + state: TurnState.Complete, + }], + } as SessionState); + + const session = await sessionHandler.provideChatSessionContent(sessionResource, CancellationToken.None); + disposables.add(toDisposable(() => session.dispose())); + + const request = session.history[0]; + assert.strictEqual(request.type, 'request'); + if (request.type === 'request') { + assert.deepStrictEqual(request.variableData?.variables.map(variable => ({ + kind: variable.kind, + id: variable.id, + name: variable.name, + value: variable.value, + _meta: variable._meta, + })), [ + toAgentHostCompletionVariableEntry(AgentHostCompletionReferenceKind.Skill, '/agent-host-docs', skillMeta.uri, skillMeta), + toAgentHostCompletionVariableEntry(AgentHostCompletionReferenceKind.Command, '/rename', 'rename', commandMeta), + ]); + } + }); + test('untitled sessions have empty history', async () => { const { sessionHandler } = createContribution(disposables); diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAttachmentsContentPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAttachmentsContentPart.test.ts index 3e650dceddc216..041587600bd8ae 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAttachmentsContentPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAttachmentsContentPart.test.ts @@ -10,11 +10,10 @@ import { URI } from '../../../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; import { workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js'; import { ChatAttachmentsContentPart } from '../../../../browser/widget/chatContentParts/chatAttachmentsContentPart.js'; -import { IChatRequestVariableEntry } from '../../../../common/attachments/chatVariableEntries.js'; +import { AgentHostCompletionReferenceKind, IChatRequestVariableEntry, toAgentHostCompletionVariableEntry } from '../../../../common/attachments/chatVariableEntries.js'; suite('ChatAttachmentsContentPart', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); - let disposables: DisposableStore; let instantiationService: ReturnType; @@ -206,6 +205,45 @@ suite('ChatAttachmentsContentPart', () => { assert.strictEqual(attachments.length, 2, 'Should render 2 file attachments'); }); + test('should not render agent host completion references as attachments', () => { + const variables: IChatRequestVariableEntry[] = [ + createFileEntry('file1.ts'), + toAgentHostCompletionVariableEntry(AgentHostCompletionReferenceKind.Command, '/rename', 'rename', undefined), + toAgentHostCompletionVariableEntry(AgentHostCompletionReferenceKind.Skill, '/agent-host-docs', 'file:///skills/agent-host-docs/SKILL.md', undefined), + ]; + + const part = store.add(instantiationService.createInstance( + ChatAttachmentsContentPart, + { variables } + )); + + mainWindow.document.body.appendChild(part.domNode!); + disposables.add(toDisposable(() => part.domNode?.remove())); + + const attachments = part.domNode!.querySelectorAll('.chat-attached-context-attachment'); + assert.strictEqual(attachments.length, 1, 'Should only render the file attachment'); + }); + + test('should not count agent host completion references in show more label', () => { + const variables: IChatRequestVariableEntry[] = [ + createFileEntry('file1.ts'), + toAgentHostCompletionVariableEntry(AgentHostCompletionReferenceKind.Command, '/rename', 'rename', undefined), + toAgentHostCompletionVariableEntry(AgentHostCompletionReferenceKind.Skill, '/agent-host-docs', 'file:///skills/agent-host-docs/SKILL.md', undefined), + createFileEntry('file2.ts'), + ]; + + const part = store.add(instantiationService.createInstance( + ChatAttachmentsContentPart, + { variables, limit: 1 } + )); + + mainWindow.document.body.appendChild(part.domNode!); + disposables.add(toDisposable(() => part.domNode?.remove())); + + const showMoreLabel = part.domNode!.querySelector('.chat-attachments-show-more-button .chat-attached-context-custom-text')?.textContent; + assert.strictEqual(showMoreLabel, '1 more'); + }); + test('should have chat-attached-context class on domNode', () => { const variables: IChatRequestVariableEntry[] = [createFileEntry('file.ts')];