Path: blob/main/src/vs/workbench/contrib/chat/browser/attachments/chatInputRelatedFilesContrib.ts
4780 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { CancellationToken } from '../../../../../base/common/cancellation.js';6import { Emitter, Event } from '../../../../../base/common/event.js';7import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';8import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js';9import { autorun } from '../../../../../base/common/observable.js';10import { isEqual } from '../../../../../base/common/resources.js';11import { URI } from '../../../../../base/common/uri.js';12import { localize } from '../../../../../nls.js';13import { IWorkbenchContribution } from '../../../../common/contributions.js';14import { IChatEditingService, IChatEditingSession } from '../../common/editing/chatEditingService.js';15import { IChatWidget, IChatWidgetService } from '../chat.js';1617export class ChatRelatedFilesContribution extends Disposable implements IWorkbenchContribution {18static readonly ID = 'chat.relatedFilesWorkingSet';1920private readonly chatEditingSessionDisposables = new ResourceMap<DisposableStore>();21private _currentRelatedFilesRetrievalOperation: Promise<void> | undefined;2223constructor(24@IChatEditingService private readonly chatEditingService: IChatEditingService,25@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,26) {27super();2829this._register(autorun((reader) => {30const sessions = this.chatEditingService.editingSessionsObs.read(reader);31sessions.forEach(session => {32const widget = this.chatWidgetService.getWidgetBySessionResource(session.chatSessionResource);33if (widget && !this.chatEditingSessionDisposables.has(session.chatSessionResource)) {34this._handleNewEditingSession(session, widget);35}36});37}));38}3940private _updateRelatedFileSuggestions(currentEditingSession: IChatEditingSession, widget: IChatWidget) {41if (this._currentRelatedFilesRetrievalOperation) {42return;43}4445const workingSetEntries = currentEditingSession.entries.get();46if (workingSetEntries.length > 0 || widget.attachmentModel.fileAttachments.length === 0) {47// Do this only for the initial working set state48return;49}5051this._currentRelatedFilesRetrievalOperation = this.chatEditingService.getRelatedFiles(currentEditingSession.chatSessionResource, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None)52.then((files) => {53if (!files?.length || !widget.viewModel || !widget.input.relatedFiles) {54return;55}5657const currentEditingSession = this.chatEditingService.getEditingSession(widget.viewModel.sessionResource);58if (!currentEditingSession || currentEditingSession.entries.get().length) {59return; // Might have disposed while we were calculating60}6162const existingFiles = new ResourceSet([...widget.attachmentModel.fileAttachments, ...widget.input.relatedFiles.removedFiles]);63if (!existingFiles.size) {64return;65}6667// Pick up to 2 related files68const newSuggestions = new ResourceMap<string>();69for (const group of files) {70for (const file of group.files) {71if (newSuggestions.size >= 2) {72break;73}74if (existingFiles.has(file.uri)) {75continue;76}77newSuggestions.set(file.uri, localize('relatedFile', "{0} (Suggested)", file.description));78existingFiles.add(file.uri);79}80}8182widget.input.relatedFiles.value = [...newSuggestions.entries()].map(([uri, description]) => ({ uri, description }));83})84.finally(() => {85this._currentRelatedFilesRetrievalOperation = undefined;86});8788}8990private _handleNewEditingSession(currentEditingSession: IChatEditingSession, widget: IChatWidget) {91const disposableStore = new DisposableStore();92disposableStore.add(currentEditingSession.onDidDispose(() => {93disposableStore.clear();94}));95this._updateRelatedFileSuggestions(currentEditingSession, widget);96const onDebouncedType = Event.debounce(widget.inputEditor.onDidChangeModelContent, () => null, 3000);97disposableStore.add(onDebouncedType(() => {98this._updateRelatedFileSuggestions(currentEditingSession, widget);99}));100disposableStore.add(widget.attachmentModel.onDidChange(() => {101this._updateRelatedFileSuggestions(currentEditingSession, widget);102}));103disposableStore.add(currentEditingSession.onDidDispose(() => {104disposableStore.dispose();105}));106disposableStore.add(widget.onDidAcceptInput(() => {107widget.input.relatedFiles?.clear();108this._updateRelatedFileSuggestions(currentEditingSession, widget);109}));110this.chatEditingSessionDisposables.set(currentEditingSession.chatSessionResource, disposableStore);111}112113override dispose() {114for (const store of this.chatEditingSessionDisposables.values()) {115store.dispose();116}117super.dispose();118}119}120121export interface IChatRelatedFile {122uri: URI;123description: string;124}125export class ChatRelatedFiles extends Disposable {126127private readonly _onDidChange = this._register(new Emitter<void>());128readonly onDidChange: Event<void> = this._onDidChange.event;129130private _removedFiles = new ResourceSet();131get removedFiles() {132return this._removedFiles;133}134135private _value: IChatRelatedFile[] = [];136get value() {137return this._value;138}139140set value(value: IChatRelatedFile[]) {141this._value = value;142this._onDidChange.fire();143}144145remove(uri: URI) {146this._value = this._value.filter(file => !isEqual(file.uri, uri));147this._removedFiles.add(uri);148this._onDidChange.fire();149}150151clearRemovedFiles() {152this._removedFiles.clear();153}154155clear() {156this._value = [];157this._removedFiles.clear();158this._onDidChange.fire();159}160}161162163