ignore prompt files for implicit context

This commit is contained in:
Martin Aeschlimann
2025-06-12 17:18:03 +02:00
parent 0fd5ef7300
commit 79ff867c2e
9 changed files with 52 additions and 197 deletions
@@ -6,12 +6,10 @@
import * as dom from '../../../../../base/browser/dom.js';
import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';
import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js';
import { Codicon } from '../../../../../base/common/codicons.js';
import { KeyCode } from '../../../../../base/common/keyCodes.js';
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
import { Schemas } from '../../../../../base/common/network.js';
import { basename, dirname } from '../../../../../base/common/resources.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { URI } from '../../../../../base/common/uri.js';
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
import { IModelService } from '../../../../../editor/common/services/model.js';
@@ -61,9 +59,7 @@ export class ImplicitContextAttachmentWidget extends Disposable {
const file = URI.isUri(this.attachment.value) ? this.attachment.value : this.attachment.value!.uri;
const range = undefined;
const attachmentTypeName = (this.attachment.isPromptFile === false)
? file.scheme === Schemas.vscodeNotebookCell ? localize('cell.lowercase', "cell") : localize('file.lowercase', "file")
: localize('prompt.lowercase', "prompt");
const attachmentTypeName = file.scheme === Schemas.vscodeNotebookCell ? localize('cell.lowercase', "cell") : localize('file.lowercase', "file");
const fileBasename = basename(file);
const fileDirname = dirname(file);
@@ -74,16 +70,11 @@ export class ImplicitContextAttachmentWidget extends Disposable {
const currentFile = localize('openEditor', "Suggested context (current file)");
const title = `${currentFile}\n${uriLabel}`;
const icon = this.attachment.isPromptFile
? ThemeIcon.fromId(Codicon.bookmark.id)
: undefined;
label.setFile(file, {
fileKind: FileKind.FILE,
hidePath: true,
range,
title,
icon,
title
});
this.domNode.ariaLabel = ariaLabel;
this.domNode.tabIndex = 0;
@@ -25,7 +25,8 @@ import { createNotebookOutputVariableEntry, NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_
import { getOutputViewModelFromId } from '../../notebook/browser/controller/cellOutputActions.js';
import { getNotebookEditorFromEditorPane } from '../../notebook/browser/notebookBrowser.js';
import { CHAT_ATTACHABLE_IMAGE_MIME_TYPES, getAttachableImageExtension } from '../common/chatModel.js';
import { IChatRequestVariableEntry, OmittedState, IDiagnosticVariableEntry, IDiagnosticVariableEntryFilterData, ISymbolVariableEntry } from '../common/chatVariableEntries.js';
import { IChatRequestVariableEntry, OmittedState, IDiagnosticVariableEntry, IDiagnosticVariableEntryFilterData, ISymbolVariableEntry, toPromptFileVariableEntry } from '../common/chatVariableEntries.js';
import { getPromptsTypeForLanguageId } from '../common/promptSyntax/promptTypes.js';
import { imageToHash } from './chatPasteProviders.js';
import { resizeImage } from './imageUtils.js';
@@ -83,8 +84,11 @@ export async function resolveResourceAttachContext(resource: URI, isDirectory: b
let omittedState = OmittedState.NotOmitted;
if (!isDirectory) {
let languageId: string | undefined;
try {
const createdModel = await textModelService.createModelReference(resource);
languageId = createdModel.object.getLanguageId();
createdModel.dispose();
} catch {
omittedState = OmittedState.Full;
@@ -93,6 +97,9 @@ export async function resolveResourceAttachContext(resource: URI, isDirectory: b
if (/\.(svg)$/i.test(resource.path)) {
omittedState = OmittedState.Full;
}
if (languageId && getPromptsTypeForLanguageId(languageId)) {
return toPromptFileVariableEntry(resource, true);
}
}
return {
@@ -482,7 +482,6 @@ export class PromptFileAttachmentWidget extends AbstractChatAttachmentWidget {
constructor(
resource: URI,
attachment: IChatRequestVariableEntry,
correspondingContentReference: IChatContentReference | undefined,
currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined,
options: { shouldFocusClearButton: boolean; supportsDeletion: boolean },
container: HTMLElement,
@@ -497,7 +496,7 @@ export class PromptFileAttachmentWidget extends AbstractChatAttachmentWidget {
super(attachment, options, container, contextResourceLabels, hoverDelegate, currentLanguageModel, commandService, openerService);
this.hintElement = dom.append(this.element, dom.$('span.chat-implicit-hint'));
this.hintElement = dom.append(this.element, dom.$('span.prompt-type'));
this.updateLabel(resource);
@@ -12,9 +12,9 @@ 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 { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isSCMHistoryItemVariableEntry, OmittedState } from '../../common/chatVariableEntries.js';
import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isSCMHistoryItemVariableEntry, OmittedState } from '../../common/chatVariableEntries.js';
import { ChatResponseReferencePartStatusKind, IChatContentReference } from '../../common/chatService.js';
import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, SCMHistoryItemAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from '../chatAttachmentWidgets.js';
import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, SCMHistoryItemAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from '../chatAttachmentWidgets.js';
export class ChatAttachmentsContentPart extends Disposable {
private readonly attachedContextDisposables = this._register(new DisposableStore());
@@ -59,6 +59,8 @@ export class ChatAttachmentsContentPart extends Disposable {
} else if (isImageVariableEntry(attachment)) {
attachment.omittedState = isAttachmentPartialOrOmitted ? OmittedState.Full : attachment.omittedState;
widget = this.instantiationService.createInstance(ImageAttachmentWidget, resource, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate);
} else if (resource && isPromptFileVariableEntry(attachment)) {
widget = this.instantiationService.createInstance(PromptFileAttachmentWidget, resource, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate);
} else if (resource && (attachment.kind === 'file' || attachment.kind === 'directory')) {
widget = this.instantiationService.createInstance(FileAttachmentWidget, resource, range, attachment, correspondingContentReference, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate);
} else if (isPasteVariableEntry(attachment)) {
@@ -203,16 +203,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
*/
get hasPromptFileAttachments(): boolean {
// if prompt attached explicitly as a "prompt" attachment
if (this._attachmentModel.hasPromptFiles(PROMPT_LANGUAGE_ID)) {
return true;
}
if (this.implicitContext === undefined) {
return false;
}
// if prompt attached as an implicit "current file" context
return (this.implicitContext.isPromptFile && this.implicitContext.enabled);
return this._attachmentModel.hasPromptFiles(PROMPT_LANGUAGE_ID);
}
private _indexOfLastAttachedContextDeletedWithKeyboard: number;
@@ -459,12 +450,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
this._hasFileAttachmentContextKey = ChatContextKeys.hasFileAttachments.bindTo(contextKeyService);
// trigger re-layout of chat input when number of instruction attachment changes
// this._register(this._attachmentModel.promptInstructions.onUpdate(() => {
// this._handleAttachedContextChange();
// this._onDidChangeHeight.fire();
// }));
this.initSelectedModel();
this._register(this.onDidChangeCurrentChatMode(() => this.accessibilityService.alert(this._currentMode.kind)));
@@ -1299,7 +1284,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
} else if (resource && isNotebookOutputVariableEntry(attachment)) {
attachmentWidget = this.instantiationService.createInstance(NotebookCellOutputChatAttachmentWidget, resource, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate);
} else if (resource && isPromptFileVariableEntry(attachment)) {
attachmentWidget = this.instantiationService.createInstance(PromptFileAttachmentWidget, resource, attachment, undefined, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate);
attachmentWidget = this.instantiationService.createInstance(PromptFileAttachmentWidget, resource, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate);
} else if (resource && (attachment.kind === 'file' || attachment.kind === 'directory')) {
attachmentWidget = this.instantiationService.createInstance(FileAttachmentWidget, resource, range, attachment, undefined, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate);
} else if (isImageVariableEntry(attachment)) {
@@ -13,20 +13,17 @@ import { URI } from '../../../../../base/common/uri.js';
import { getCodeEditor, ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';
import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';
import { Location } from '../../../../../editor/common/languages.js';
import { IModelService } from '../../../../../editor/common/services/model.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { ILogService } from '../../../../../platform/log/common/log.js';
import { IWorkbenchContribution } from '../../../../common/contributions.js';
import { EditorsOrder } from '../../../../common/editor.js';
import { IEditorService } from '../../../../services/editor/common/editorService.js';
import { getNotebookEditorFromEditorPane, INotebookEditor } from '../../../notebook/browser/notebookBrowser.js';
import { IChatEditingService } from '../../common/chatEditingService.js';
import { IChatRequestImplicitVariableEntry, IChatRequestVariableEntry, toPromptFileVariableEntry } from '../../common/chatVariableEntries.js';
import { IChatRequestImplicitVariableEntry, IChatRequestVariableEntry } from '../../common/chatVariableEntries.js';
import { IChatService } from '../../common/chatService.js';
import { ChatAgentLocation } from '../../common/constants.js';
import { ILanguageModelIgnoredFilesService } from '../../common/ignoredFiles.js';
import { PROMPT_LANGUAGE_ID } from '../../common/promptSyntax/promptTypes.js';
import { IPromptsService, TSharedPrompt } from '../../common/promptSyntax/service/promptsService.js';
import { getPromptsTypeForLanguageId } from '../../common/promptSyntax/promptTypes.js';
import { IChatWidget, IChatWidgetService } from '../chat.js';
export class ChatImplicitContextContribution extends Disposable implements IWorkbenchContribution {
@@ -155,6 +152,7 @@ export class ChatImplicitContextContribution extends Disposable implements IWork
let languageId: string | undefined;
if (model) {
newValue = model.uri;
languageId = model.getLanguageId();
}
const notebookEditor = this.findActiveNotebookEditor();
@@ -174,6 +172,8 @@ export class ChatImplicitContextContribution extends Disposable implements IWork
return;
}
const isPromptFile = languageId && getPromptsTypeForLanguageId(languageId) !== undefined;
const widgets = updateWidget ? [updateWidget] : [...this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Panel), ...this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Editor)];
for (const widget of widgets) {
if (!widget.input.implicitContext) {
@@ -181,11 +181,9 @@ export class ChatImplicitContextContribution extends Disposable implements IWork
}
const setting = this._implicitContextEnablement[widget.location];
const isFirstInteraction = widget.viewModel?.getItems().length === 0;
if (setting === 'first' && !isFirstInteraction) {
widget.input.implicitContext.setValue(undefined, false, undefined);
} else if (setting === 'always' || setting === 'first' && isFirstInteraction) {
if ((setting === 'always' || setting === 'first' && isFirstInteraction) && !isPromptFile) { // disable implicit context for prompt files
widget.input.implicitContext.setValue(newValue, isSelection, languageId);
} else if (setting === 'never') {
} else {
widget.input.implicitContext.setValue(undefined, false, undefined);
}
}
@@ -193,18 +191,8 @@ export class ChatImplicitContextContribution extends Disposable implements IWork
}
export class ChatImplicitContext extends Disposable implements IChatRequestImplicitVariableEntry {
/**
* If the implicit context references a prompt file, this field
* holds a reference to an associated prompt parser instance.
*/
private prompt: TSharedPrompt | undefined;
get id() {
if (this.prompt !== undefined) {
const variable = toPromptFileVariableEntry(this.prompt.uri, true);
return variable.id;
}
if (URI.isUri(this.value)) {
return 'vscode.implicit.file';
} else if (this.value) {
@@ -219,12 +207,6 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli
}
get name(): string {
if (this.prompt !== undefined) {
const variable = toPromptFileVariableEntry(this.prompt.uri, true);
return variable.name;
}
if (URI.isUri(this.value)) {
return `file:${basename(this.value)}`;
} else if (this.value) {
@@ -237,12 +219,6 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli
readonly kind = 'implicit';
get modelDescription(): string {
if (this.prompt !== undefined) {
const variable = toPromptFileVariableEntry(this.prompt.uri, true);
return variable.modelDescription;
}
if (URI.isUri(this.value)) {
return `User's active file`;
} else if (this._isSelection) {
@@ -277,90 +253,20 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli
this._onDidChangeValue.fire();
}
constructor(
@IPromptsService private readonly promptsService: IPromptsService,
@IModelService private readonly modelService: IModelService,
@ILogService private readonly logService: ILogService,
) {
super();
}
setValue(value: Location | URI | undefined, isSelection: boolean, languageId?: string): void {
this._value = value;
this._isSelection = isSelection;
// remove and dispose existent prompt parser instance
this.removePrompt();
// if language ID is a 'prompt' language, create a prompt parser instance
if (value && (languageId === PROMPT_LANGUAGE_ID)) {
this.addPrompt(value);
}
this._onDidChangeValue.fire();
}
public async toBaseEntries(): Promise<readonly IChatRequestVariableEntry[]> {
// chat variable for non-prompt file attachment
if (this.prompt === undefined) {
return [{
kind: 'file',
id: this.id,
name: this.name,
value: this.value,
modelDescription: this.modelDescription,
}];
}
// prompt can have any number of nested references, hence
// collect all of valid ones and return the entire list
await this.prompt.allSettled();
return [
// add all valid child references in the prompt
...this.prompt.allValidReferences.map((link) => {
return toPromptFileVariableEntry(link.uri, false);
}),
// and then the root prompt reference itself
toPromptFileVariableEntry(this.prompt.uri, true)
];
return [{
kind: 'file',
id: this.id,
name: this.name,
value: this.value,
modelDescription: this.modelDescription,
}];
}
/**
* Whether the implicit context references a prompt file.
*/
public get isPromptFile() {
return (this.prompt !== undefined);
}
/**
* Add prompt parser instance for the provided value.
*/
private addPrompt(
value: URI | Location,
): void {
const uri = URI.isUri(value)
? value
: value.uri;
const model = this.modelService.getModel(uri);
const modelExists = (model !== null);
if ((modelExists === false) || model.isDisposed()) {
return this.logService.warn(
`cannot create prompt parser instance for ${uri.path} (model exists: ${modelExists})`,
);
}
this.prompt = this.promptsService.getSyntaxParserFor(model);
}
/**
* Remove and dispose prompt parser instance.
*/
private removePrompt(): void {
delete this.prompt;
}
public override dispose(): void {
this.removePrompt();
super.dispose();
}
}
@@ -1121,66 +1121,17 @@ have to be updated for changes to the rules above, or to support more deeply nes
/**
* Styles for the `reusable prompts` attachment widget.
*/
.chat-attached-context .chat-prompt-attachment {
display: flex;
gap: 4px;
}
.chat-attached-context .chat-prompt-attachment .codicon {
color: inherit;
text-decoration: none;
}
.chat-attached-context-attachment .chat-implicit-hint {
.chat-attached-context-attachment .prompt-type {
opacity: 0.7;
font-size: .9em;
margin-top: -0.5px;
margin-left: 0.5px;
}
.chat-attached-context .chat-prompt-attachment.warning {
.chat-attached-context-attachment.warning {
color: var(--vscode-notificationsWarningIcon-foreground);
}
.chat-attached-context .chat-prompt-attachment.error {
.chat-attached-context-attachment.error {
color: var(--vscode-notificationsErrorIcon-foreground);
}
.chat-attached-context .chat-prompt-attachment.disabled {
border-style: dashed;
opacity: 0.75;
}
.chat-attached-context .chat-prompt-attachment:focus .monaco-button {
border-color: var(--vscode-focusBorder);
}
.chat-attached-context .chat-prompt-attachment .monaco-icon-label-container {
margin-top: -0.1em;
}
.chat-attached-context .chat-prompt-attachment.warning.implicit {
border: 1px solid currentColor;
}
.chat-attached-context .chat-prompt-attachment.implicit .monaco-button {
padding-left: 2px;
padding-right: 2px;
}
/*
* If in one of the non-normal states, make sure the `main icon` of
* the component has the same color as the component itself
*/
.chat-attached-context .chat-prompt-attachment.error .monaco-icon-label::before,
.chat-attached-context .chat-prompt-attachment.warning .monaco-icon-label::before,
.chat-attached-context .chat-prompt-attachment.disabled .monaco-icon-label::before {
color: inherit;
}
.chat-attached-context .chat-prompt-attachment.disabled .monaco-icon-label::before {
font-style: italic;
}
.chat-attached-context .chat-prompt-attachment.disabled:hover {
opacity: 1;
}
.chat-attached-context .chat-prompt-attachment.disabled .chat-implicit-hint,
.chat-attached-context .chat-prompt-attachment.disabled .label-name {
font-style: italic;
text-decoration: line-through;
}
.chat-attached-context .chat-prompt-attachment.disabled:focus {
outline: none;
border-color: var(--vscode-focusBorder);
}
.chat-attached-context-attachment .monaco-icon-label > .monaco-icon-label-container > .monaco-icon-suffix-container > .label-suffix {
color: var(--vscode-peekViewTitleDescription-foreground);
@@ -67,7 +67,6 @@ export interface IChatRequestImplicitVariableEntry extends IBaseChatRequestVaria
readonly isFile: true;
readonly value: URI | Location | undefined;
readonly isSelection: boolean;
readonly isPromptFile: boolean;
readonly enabled: boolean;
}
@@ -46,10 +46,25 @@ export function getLanguageIdForPromptsType(type: PromptsType): string {
default:
throw new Error(`Unknown prompt type: ${type}`);
}
}/**
}
export function getPromptsTypeForLanguageId(languageId: string): PromptsType | undefined {
switch (languageId) {
case PROMPT_LANGUAGE_ID:
return PromptsType.prompt;
case INSTRUCTIONS_LANGUAGE_ID:
return PromptsType.instructions;
case MODE_LANGUAGE_ID:
return PromptsType.mode;
default:
return undefined;
}
}
/**
* What the prompt is used for.
*/
export enum PromptsType {
instructions = 'instructions',
prompt = 'prompt',