[instruction attachments]: exclude already attached instruction files from the picker dialog

legomushroom/instruction-attachments
Oleg Solomko 2024-12-27 13:26:32 -08:00
parent f2c253bedb
commit de9a42fb4c
6 changed files with 91 additions and 62 deletions

View File

@ -574,9 +574,11 @@ export class AttachContextAction extends Action2 {
toAttach.push(convertBufferToScreenshotVariable(blob));
}
} else if (isPromptInstructionsQuickPickItem(pick)) {
const { promptInstructions } = widget.attachmentModel;
// find all prompt instruction files in the user project
// and present them to the user so they can select one
const filesPromise = widget.attachmentModel.promptInstructions.listFiles()
const filesPromise = promptInstructions.listNonAttachedFiles()
.then((files) => {
return files.map((file) => {
const result: IQuickPickItem & { value: URI } = {
@ -604,7 +606,7 @@ export class AttachContextAction extends Action2 {
}
// add selected prompt instructions reference to the chat attachments model
widget.attachmentModel.promptInstructions.add(selectedFile.value);
promptInstructions.add(selectedFile.value);
} else {
// Anything else is an attachment
const attachmentPick = pick as IAttachmentQuickPickItem;

View File

@ -32,13 +32,7 @@ export class InstructionAttachmentsWidget extends Disposable {
* the possible references nested inside the children.
*/
public get references(): readonly URI[] {
const result = [];
for (const child of this.children) {
result.push(...child.references);
}
return result;
return this.model.references;
}
/**

View File

@ -17,14 +17,13 @@ import { ILabelService } from '../../../../../../platform/label/common/label.js'
import { StandardMouseEvent } from '../../../../../../base/browser/mouseEvent.js';
import { IModelService } from '../../../../../../editor/common/services/model.js';
import { IHoverService } from '../../../../../../platform/hover/browser/hover.js';
import { NonPromptSnippetFile } from '../../../common/promptFileReferenceErrors.js';
import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js';
import { ILanguageService } from '../../../../../../editor/common/languages/language.js';
import { FileKind, IFileService } from '../../../../../../platform/files/common/files.js';
import { IMenuService, MenuId } from '../../../../../../platform/actions/common/actions.js';
import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js';
import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js';
import { ChatInstructionsAttachment } from '../../chatAttachmentModel/chatInstructionsAttachment.js';
import { ChatInstructionsAttachmentModel } from '../../chatAttachmentModel/chatInstructionsAttachment.js';
import { getDefaultHoverDelegate } from '../../../../../../base/browser/ui/hover/hoverDelegateFactory.js';
import { getFlatContextMenuActions } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js';
@ -37,31 +36,6 @@ export class InstructionsAttachmentWidget extends Disposable {
*/
public readonly domNode: HTMLElement;
/**
* Get `URI` for the main reference and `URI`s of all valid
* child references it may contain.
*/
public get references(): readonly URI[] {
const { reference, enabled, errorCondition } = this.model;
// return no references if the attachment is disabled
if (!enabled) {
return [];
}
// if the model has an error, return no references
if (errorCondition && !(errorCondition instanceof NonPromptSnippetFile)) {
return [];
}
// otherwise return `URI` for the main reference and
// all valid child `URI` references it may contain
return [
...reference.validFileReferenceUris,
reference.uri,
];
}
/**
* Get the `URI` associated with the model reference.
*/
@ -91,7 +65,7 @@ export class InstructionsAttachmentWidget extends Disposable {
private readonly renderDisposables = this._register(new DisposableStore());
constructor(
private readonly model: ChatInstructionsAttachment,
private readonly model: ChatInstructionsAttachmentModel,
private readonly resourceLabels: ResourceLabels,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IContextMenuService private readonly contextMenuService: IContextMenuService,
@ -123,7 +97,7 @@ export class InstructionsAttachmentWidget extends Disposable {
this.renderDisposables.clear();
this.domNode.classList.remove('warning', 'error', 'disabled');
const { enabled, errorCondition } = this.model;
const { enabled, resolveIssue: errorCondition } = this.model;
if (!enabled) {
this.domNode.classList.add('disabled');
}

View File

@ -5,8 +5,8 @@
import { URI } from '../../../../../base/common/uri.js';
import { Emitter } from '../../../../../base/common/event.js';
import { ChatInstructionsAttachment } from './chatInstructionsAttachment.js';
import { ChatInstructionsFileLocator } from './chatInstructionsFileLocator.js';
import { ChatInstructionsAttachmentModel } from './chatInstructionsAttachment.js';
import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
@ -19,7 +19,7 @@ const PROMPT_INSTRUCTIONS_SETTING_NAME = 'chat.experimental.prompt-instructions.
/**
* Model for a collection of prompt instruction attachments.
* See {@linkcode ChatInstructionsAttachment}.
* See {@linkcode ChatInstructionsAttachmentModel}.
*/
export class ChatInstructionAttachmentsModel extends Disposable {
/**
@ -30,9 +30,23 @@ export class ChatInstructionAttachmentsModel extends Disposable {
/**
* List of all prompt instruction attachments.
*/
private attachments: DisposableMap<string, ChatInstructionsAttachment> =
private attachments: DisposableMap<string, ChatInstructionsAttachmentModel> =
this._register(new DisposableMap());
/**
* Get all `URI`s of all valid references, including all
* the possible references nested inside the children.
*/
public get references(): readonly URI[] {
const result = [];
for (const child of this.attachments.values()) {
result.push(...child.references);
}
return result;
}
/**
* Event that fires then this model is updated.
*
@ -53,13 +67,13 @@ export class ChatInstructionAttachmentsModel extends Disposable {
* Event that fires when a new prompt instruction attachment is added.
* See {@linkcode onAdd}.
*/
protected _onAdd = this._register(new Emitter<ChatInstructionsAttachment>());
protected _onAdd = this._register(new Emitter<ChatInstructionsAttachmentModel>());
/**
* The `onAdd` event fires when a new prompt instruction attachment is added.
*
* @param callback Function to invoke on add.
*/
public onAdd(callback: (attachment: ChatInstructionsAttachment) => unknown): this {
public onAdd(callback: (attachment: ChatInstructionsAttachmentModel) => unknown): this {
this._register(this._onAdd.event(callback));
return this;
@ -85,7 +99,7 @@ export class ChatInstructionAttachmentsModel extends Disposable {
return this;
}
const instruction = this.initService.createInstance(ChatInstructionsAttachment, uri)
const instruction = this.initService.createInstance(ChatInstructionsAttachmentModel, uri)
.onUpdate(this._onUpdate.fire)
.onDispose(() => {
this.attachments.deleteAndDispose(uri.path);
@ -118,10 +132,10 @@ export class ChatInstructionAttachmentsModel extends Disposable {
}
/**
* List all prompt instruction files available.
* List prompt instruction files available and not attached yet.
*/
public async listFiles(): Promise<readonly URI[]> {
return await this.instructionsFileReader.listFiles();
public async listNonAttachedFiles(): Promise<readonly URI[]> {
return await this.instructionsFileReader.listFiles(this.references);
}
/**

View File

@ -16,7 +16,7 @@ import { FileOpenFailed, NonPromptSnippetFile, RecursiveReference } from '../../
* Object that represents an error that may occur during
* the process of resolving prompt instructions reference.
*/
export interface IErrorCondition {
interface IIssue {
/**
* Type of the failure. Currently all errors that occur on
* the "main" root reference directly attached to the chat
@ -26,15 +26,15 @@ export interface IErrorCondition {
type: 'error' | 'warning';
/**
* Error message.
* Error or warning message.
*/
message: string;
}
/**
* Chat prompt instructions attachment.
* Model for a single chat prompt instructions attachment.
*/
export class ChatInstructionsAttachment extends Disposable {
export class ChatInstructionsAttachmentModel extends Disposable {
/**
* Private reference of the underlying prompt instructions
* reference instance.
@ -47,11 +47,40 @@ export class ChatInstructionsAttachment extends Disposable {
return this._reference;
}
/**
* If the prompt instructions reference has failed to resolve, this
* field error that contains failure details, otherwise `undefined`.
* Get `URI` for the main reference and `URI`s of all valid
* child references it may contain.
*/
public get errorCondition(): IErrorCondition | undefined {
public get references(): readonly URI[] {
const { reference, enabled, resolveIssue } = this;
// return no references if the attachment is disabled
if (!enabled) {
return [];
}
// if the model has an error, return no references
if (resolveIssue && !(resolveIssue instanceof NonPromptSnippetFile)) {
return [];
}
// otherwise return `URI` for the main reference and
// all valid child `URI` references it may contain
return [
...reference.validFileReferenceUris,
reference.uri,
];
}
/**
* If the prompt instructions reference (or any of its child references) has
* failed to resolve, this field contains the failure details, otherwise `undefined`.
*
* See {@linkcode IIssue}.
*/
public get resolveIssue(): IIssue | undefined {
const { errorCondition } = this._reference;
const errorConditions = this.collectErrorConditions();
@ -72,7 +101,7 @@ export class ChatInstructionsAttachment extends Disposable {
? `\n-\n +${restErrors.length} more error${restErrors.length > 1 ? 's' : ''}`
: '';
const errorMessage = this.getErrorMessage(firstError, isRootError);
const errorMessage = this.getMessage(firstError, isRootError);
return {
type,
message: `${errorMessage}${moreSuffix}`,
@ -80,13 +109,13 @@ export class ChatInstructionsAttachment extends Disposable {
}
/**
* Get error message for the provided error condition object.
* Get message for the provided error condition object.
*
* @param error Error object.
* @param isRootError If the error happened on the the "main" root reference.
* @returns Error message.
*/
private getErrorMessage(
private getMessage(
error: TErrorCondition,
isRootError: boolean,
): string {

View File

@ -36,13 +36,23 @@ export class ChatInstructionsFileLocator {
/**
* List all prompt instructions files from the filesystem.
*
* @param exclude List of `URIs` to exclude from the result.
* @returns List of prompt instructions files found in the workspace.
*/
public async listFiles(): Promise<readonly URI[]> {
const locations = this.getSourceLocations();
const result = await this.findInstructionsFiles(locations);
public async listFiles(exclude: ReadonlyArray<URI>): Promise<readonly URI[]> {
// create a set from the list of URIs for convenience
const excludeSet: Set<string> = new Set();
for (const excludeUri of exclude) {
excludeSet.add(excludeUri.path);
}
return result;
// filter out the excluded paths from the locations list
const locations = this.getSourceLocations()
.filter((location) => {
return !excludeSet.has(location.path);
});
return await this.findInstructionFiles(locations, excludeSet);
}
/**
@ -108,10 +118,12 @@ export class ChatInstructionsFileLocator {
* Finds all existent prompt instruction files in the provided locations.
*
* @param locations List of locations to search for prompt instruction files in.
* @param exclude Map of `path -> boolean` to exclude from the result.
* @returns List of prompt instruction files found in the provided locations.
*/
private async findInstructionsFiles(
private async findInstructionFiles(
locations: readonly URI[],
exclude: ReadonlySet<string>,
): Promise<readonly URI[]> {
const results = await this.fileService.resolveAll(
locations.map((location) => {
@ -142,6 +154,10 @@ export class ChatInstructionsFileLocator {
continue;
}
if (exclude.has(resource.path)) {
continue;
}
files.push(resource);
}
}