diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContext.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContext.ts index 7d8a91898f6..80d3960d635 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContext.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContext.ts @@ -188,16 +188,16 @@ class RelatedFilesContextPickerPick implements IChatContextPickerItem { asPicker(widget: IChatWidget): IChatContextPicker { const picks = (async () => { - const chatSessionId = widget.viewModel?.sessionId; - if (!chatSessionId) { + const chatSessionResource = widget.viewModel?.sessionResource; + if (!chatSessionResource) { return []; } - const relatedFiles = await this._chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None); + const relatedFiles = await this._chatEditingService.getRelatedFiles(chatSessionResource, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None); if (!relatedFiles) { return []; } const attachments = widget.attachmentModel.getAttachmentIDs(); - return this._chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None) + return this._chatEditingService.getRelatedFiles(chatSessionResource, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None) .then((files) => (files ?? []).reduce<(IChatContextPickerPickItem | IQuickPickSeparator)[]>((acc, cur) => { acc.push({ type: 'separator', label: cur.group }); for (const file of cur.files) { diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 9c56ba31904..506385fd0bd 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -23,7 +23,6 @@ import { IChatMode } from '../common/chatModes.js'; import { IParsedChatRequest } from '../common/chatParserTypes.js'; import { CHAT_PROVIDER_ID } from '../common/chatParticipantContribTypes.js'; import { IChatElicitationRequest, IChatLocationData, IChatSendRequestOptions } from '../common/chatService.js'; -import { LocalChatSessionUri } from '../common/chatUri.js'; import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel } from '../common/chatViewModel.js'; import { ChatAgentLocation, ChatModeKind } from '../common/constants.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js'; @@ -60,9 +59,9 @@ export async function showChatWidgetInViewOrEditor(accessor: ServicesAccessor, w if ('viewId' in widget.viewContext) { await accessor.get(IViewsService).openView(widget.location); } else { - const sessionId = widget.viewModel?.sessionId; - if (sessionId) { - const existing = findExistingChatEditorByUri(LocalChatSessionUri.forSession(sessionId), accessor.get(IEditorGroupsService)); + const sessionResource = widget.viewModel?.sessionResource; + if (sessionResource) { + const existing = findExistingChatEditorByUri(sessionResource, accessor.get(IEditorGroupsService)); if (existing) { existing.group.openEditor(existing.editor); } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts index 4bad64ddf13..d5b49feacb9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts @@ -26,6 +26,7 @@ import { defaultButtonStyles } from '../../../../../platform/theme/browser/defau import { IHostService } from '../../../../services/host/browser/host.js'; import { IChatWidgetService, showChatWidgetInViewOrEditor } from '../chat.js'; import { IChatContentPartRenderContext } from './chatContentParts.js'; +import { URI } from '../../../../../base/common/uri.js'; export interface IChatConfirmationButton { label: string; @@ -118,13 +119,13 @@ class ChatConfirmationNotifier extends Disposable { super(); } - async notify(targetWindow: Window, sessionId: string): Promise { + async notify(targetWindow: Window, sessionResource: URI): Promise { // Focus Window this._hostService.focus(targetWindow, { mode: FocusMode.Notify }); // Notify - const widget = this._chatWidgetService.getWidgetBySessionId(sessionId); + const widget = this._chatWidgetService.getWidgetBySessionResource(sessionResource); const title = widget?.viewModel?.model.title ? localize('chatTitle', "Chat: {0}", widget.viewModel.model.title) : localize('chat.untitledChat', "Untitled Chat"); const notification = await dom.triggerNotification(title, { @@ -285,7 +286,7 @@ abstract class BaseSimpleChatConfirmationWidget extends Disposable { if (this.showingButtons && this._configurationService.getValue('chat.notifyWindowOnConfirmation') && !this.silent) { const targetWindow = dom.getWindow(listContainer); if (!targetWindow.document.hasFocus()) { - this.notificationManager.notify(targetWindow, this.context.element.sessionId); + this.notificationManager.notify(targetWindow, this.context.element.sessionResource); } } } @@ -481,7 +482,7 @@ abstract class BaseChatConfirmationWidget extends Disposable { if (this.showingButtons && this._configurationService.getValue('chat.notifyWindowOnConfirmation')) { const targetWindow = dom.getWindow(listContainer); if (!targetWindow.document.hasFocus()) { - this.notificationManager.notify(targetWindow, this._context.element.sessionId); + this.notificationManager.notify(targetWindow, this._context.element.sessionResource); } } } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTodoListWidget.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTodoListWidget.ts index 1e899ffc48f..0d19b7340f7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTodoListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTodoListWidget.ts @@ -18,6 +18,8 @@ import { WorkbenchList } from '../../../../../platform/list/browser/listService. import { IChatTodoListService, IChatTodo } from '../../common/chatTodoListService.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { TodoListToolDescriptionFieldSettingId } from '../../common/tools/manageTodoListTool.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { isEqual } from '../../../../../base/common/resources.js'; class TodoListDelegate implements IListVirtualDelegate { getHeight(element: IChatTodo): number { @@ -131,7 +133,7 @@ export class ChatTodoListWidget extends Disposable { private todoListContainer!: HTMLElement; private clearButtonContainer!: HTMLElement; private clearButton!: Button; - private _currentSessionId: string | undefined; + private _currentSessionResource: URI | undefined; private _todoList: WorkbenchList | undefined; constructor( @@ -217,27 +219,27 @@ export class ChatTodoListWidget extends Disposable { })); } - public render(sessionId: string | undefined): void { - if (!sessionId) { + public render(sessionResource: URI | undefined): void { + if (!sessionResource) { this.domNode.style.display = 'none'; this._onDidChangeHeight.fire(); return; } - if (this._currentSessionId !== sessionId) { + if (!isEqual(this._currentSessionResource, sessionResource)) { this._userManuallyExpanded = false; - this._currentSessionId = sessionId; + this._currentSessionResource = sessionResource; } this.updateTodoDisplay(); } - public clear(sessionId: string | undefined, force: boolean = false): void { - if (!sessionId || this.domNode.style.display === 'none') { + public clear(sessionResource: URI | undefined, force: boolean = false): void { + if (!sessionResource || this.domNode.style.display === 'none') { return; } - const currentTodos = this.chatTodoListService.getTodos(sessionId); + const currentTodos = this.chatTodoListService.getTodos(sessionResource); const shouldClear = force || !currentTodos.some(todo => todo.status !== 'completed'); if (shouldClear) { this.clearAllTodos(); @@ -245,11 +247,11 @@ export class ChatTodoListWidget extends Disposable { } private updateTodoDisplay(): void { - if (!this._currentSessionId) { + if (!this._currentSessionResource) { return; } - const todoList = this.chatTodoListService.getTodos(this._currentSessionId); + const todoList = this.chatTodoListService.getTodos(this._currentSessionResource); const shouldShow = todoList.length > 2; if (!shouldShow) { @@ -341,8 +343,8 @@ export class ChatTodoListWidget extends Disposable { this.todoListContainer.style.display = this._isExpanded ? 'block' : 'none'; - if (this._currentSessionId) { - const todoList = this.chatTodoListService.getTodos(this._currentSessionId); + if (this._currentSessionResource) { + const todoList = this.chatTodoListService.getTodos(this._currentSessionResource); const titleElement = this.expandoButton.element.querySelector('.todo-list-title') as HTMLElement; if (titleElement) { this.updateTitleElement(titleElement, todoList); @@ -353,21 +355,21 @@ export class ChatTodoListWidget extends Disposable { } private clearAllTodos(): void { - if (!this._currentSessionId) { + if (!this._currentSessionResource) { return; } - this.chatTodoListService.setTodos(this._currentSessionId, []); + this.chatTodoListService.setTodos(this._currentSessionResource, []); this.domNode.style.display = 'none'; this._onDidChangeHeight.fire(); } private updateClearButtonState(): void { - if (!this._currentSessionId) { + if (!this._currentSessionResource) { return; } - const todoList = this.chatTodoListService.getTodos(this._currentSessionId); + const todoList = this.chatTodoListService.getTodos(this._currentSessionResource); const hasInProgressTask = todoList.some(todo => todo.status === 'in-progress'); const isRequestInProgress = ChatContextKeys.requestInProgress.getValue(this.contextKeyService) ?? false; const shouldDisable = isRequestInProgress && hasInProgressTask; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts index f8afeb778a8..93445959639 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts @@ -346,7 +346,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic }); } - async getRelatedFiles(chatSessionId: string, prompt: string, files: URI[], token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined> { + async getRelatedFiles(chatSessionResource: URI, prompt: string, files: URI[], token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined> { const providers = Array.from(this._chatRelatedFilesProviders.values()); const result = await Promise.all(providers.map(async provider => { try { diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index f0e872d7d7f..68684ec5d9a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -1785,7 +1785,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - async renderChatTodoListWidget(chatSessionId: string) { + async renderChatTodoListWidget(chatSessionResource: URI) { const isTodoWidgetEnabled = this.configurationService.getValue(ChatConfiguration.TodosShowWidget) !== false; if (!isTodoWidgetEnabled) { @@ -1806,11 +1806,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge })); } - this._chatInputTodoListWidget.value.render(chatSessionId); + this._chatInputTodoListWidget.value.render(chatSessionResource); } - clearTodoListWidget(sessionId: string | undefined, force: boolean): void { - this._chatInputTodoListWidget.value?.clear(sessionId, force); + clearTodoListWidget(sessionResource: URI | undefined, force: boolean): void { + this._chatInputTodoListWidget.value?.clear(sessionResource, force); } renderChatEditingSessionState(chatEditingSession: IChatEditingSession | null) { diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 64a974a0040..85198cd27f4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -160,8 +160,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer()); readonly onDidClickFollowup: Event = this._onDidClickFollowup.event; - private readonly _onDidClickRerunWithAgentOrCommandDetection = new Emitter<{ sessionId: string; requestId: string }>(); - readonly onDidClickRerunWithAgentOrCommandDetection: Event<{ sessionId: string; requestId: string }> = this._onDidClickRerunWithAgentOrCommandDetection.event; + private readonly _onDidClickRerunWithAgentOrCommandDetection = new Emitter<{ readonly sessionResource: URI; readonly requestId: string }>(); + readonly onDidClickRerunWithAgentOrCommandDetection = this._onDidClickRerunWithAgentOrCommandDetection.event; private readonly _onDidClickRequest = this._register(new Emitter()); @@ -881,7 +881,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this._onDidClickRerunWithAgentOrCommandDetection.fire({ sessionId: element.sessionId, requestId: element.id })); + const cmdPart = this.instantiationService.createInstance(ChatAgentCommandContentPart, element.slashCommand, () => this._onDidClickRerunWithAgentOrCommandDetection.fire({ sessionResource: element.sessionResource, requestId: element.id })); templateData.value.appendChild(cmdPart.domNode); parts.push(cmdPart); inlineSlashCommandRendered = true; diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 6e7c2c1b208..44b4463ada2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -95,7 +95,6 @@ import { ChatViewPane } from './chatViewPane.js'; import { ChatViewWelcomePart, IChatSuggestedPrompts, IChatViewWelcomeContent } from './viewsWelcome/chatViewWelcomeController.js'; import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; -import { LocalChatSessionUri } from '../common/chatUri.js'; const $ = dom.$; @@ -624,7 +623,7 @@ export class ChatWidget extends Disposable implements IChatWidget { const viewModel = viewModelObs.read(r); const sessions = chatEditingService.editingSessionsObs.read(r); - const session = sessions.find(candidate => candidate.chatSessionId === viewModel?.sessionId); + const session = sessions.find(candidate => isEqual(candidate.chatSessionResource, viewModel?.sessionResource)); this._editingSession.set(undefined, undefined); this.renderChatEditingSessionState(); // this is necessary to make sure we dispose previous buttons, etc. @@ -714,9 +713,9 @@ export class ChatWidget extends Disposable implements IChatWidget { this._register(this.onDidChangeParsedInput(() => this.updateChatInputContext())); - this._register(this.chatTodoListService.onDidUpdateTodos((sessionId) => { - if (this.viewModel?.sessionId === sessionId) { - this.inputPart.renderChatTodoListWidget(sessionId); + this._register(this.chatTodoListService.onDidUpdateTodos((sessionResource) => { + if (isEqual(this.viewModel?.sessionResource, sessionResource)) { + this.inputPart.renderChatTodoListWidget(sessionResource); } })); } @@ -971,7 +970,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.unlockFromCodingAgent(); } - this.inputPart.clearTodoListWidget(this.viewModel?.sessionId, true); + this.inputPart.clearTodoListWidget(this.viewModel?.sessionResource, true); // Cancel any pending widget render and hide the widget BEFORE firing onDidClear // This prevents the widget from being re-shown by any handlers triggered by the clear event this._chatSuggestNextScheduler.cancel(); @@ -1751,8 +1750,8 @@ export class ChatWidget extends Disposable implements IChatWidget { // is this used anymore? this.acceptInput(item.message); })); - this._register(this.renderer.onDidClickRerunWithAgentOrCommandDetection(item => { - const request = this.chatService.getSession(LocalChatSessionUri.forSession(item.sessionId))?.getRequests().find(candidate => candidate.id === item.requestId); + this._register(this.renderer.onDidClickRerunWithAgentOrCommandDetection(e => { + const request = this.chatService.getSession(e.sessionResource)?.getRequests().find(candidate => candidate.id === e.requestId); if (request) { const options: IChatSendRequestOptions = { noCommandDetection: true, @@ -2307,11 +2306,11 @@ export class ChatWidget extends Disposable implements IChatWidget { this._updateAgentCapabilitiesContextKeys(e.agent); } if (e.kind === 'addRequest') { - this.inputPart.clearTodoListWidget(this.viewModel?.sessionId, false); + this.inputPart.clearTodoListWidget(this.viewModel?.sessionResource, false); } // Hide widget on request removal if (e.kind === 'removeRequest') { - this.inputPart.clearTodoListWidget(this.viewModel?.sessionId, true); + this.inputPart.clearTodoListWidget(this.viewModel?.sessionResource, true); this.chatSuggestNextWidget.hide(); } // Show next steps widget when response completes (not when request starts) @@ -2328,7 +2327,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.renderer.updateViewModel(this.viewModel); this.updateChatInputContext(); - this.input.renderChatTodoListWidget(this.viewModel.sessionId); + this.input.renderChatTodoListWidget(this.viewModel.sessionResource); } getFocus(): ChatTreeItem | undefined { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts index b2f895f09bc..cfd5d82c054 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatImplicitContext.ts @@ -102,8 +102,8 @@ export class ChatImplicitContextContribution extends Disposable implements IWork this.updateImplicitContext(); } })); - this._register(this.chatService.onDidSubmitRequest(({ chatSessionId }) => { - const widget = this.chatWidgetService.getWidgetBySessionId(chatSessionId); + this._register(this.chatService.onDidSubmitRequest(({ chatSessionResource }) => { + const widget = this.chatWidgetService.getWidgetBySessionResource(chatSessionResource); if (!widget?.input.implicitContext) { return; } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index c1e1befadbb..3c76c5cbb7f 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -995,7 +995,7 @@ class BuiltinDynamicCompletions extends Disposable { // RELATED FILES if (widget.input.currentModeKind !== ChatModeKind.Ask && widget.viewModel && widget.viewModel.model.editingSession) { - const relatedFiles = (await raceTimeout(this._chatEditingService.getRelatedFiles(widget.viewModel.sessionId, widget.getInput(), widget.attachmentModel.fileAttachments, token), 200)) ?? []; + const relatedFiles = (await raceTimeout(this._chatEditingService.getRelatedFiles(widget.viewModel.sessionResource, widget.getInput(), widget.attachmentModel.fileAttachments, token), 200)) ?? []; for (const relatedFileGroup of relatedFiles) { for (const relatedFile of relatedFileGroup.files) { if (!seen.has(relatedFile.uri)) { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts index 342dab6f528..72baf565f39 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts @@ -49,7 +49,7 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben return; } - this._currentRelatedFilesRetrievalOperation = this.chatEditingService.getRelatedFiles(currentEditingSession.chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None) + this._currentRelatedFilesRetrievalOperation = this.chatEditingService.getRelatedFiles(currentEditingSession.chatSessionResource, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None) .then((files) => { if (!files?.length || !widget.viewModel?.sessionId || !widget.input.relatedFiles) { return; diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index af58d4da5a5..67c1538a0bf 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -46,7 +46,7 @@ export interface IChatEditingService { hasRelatedFilesProviders(): boolean; registerRelatedFilesProvider(handle: number, provider: IChatRelatedFilesProvider): IDisposable; - getRelatedFiles(chatSessionId: string, prompt: string, files: URI[], token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined>; + getRelatedFiles(chatSessionResource: URI, prompt: string, files: URI[], token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined>; //#endregion } diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 5b4a1e76b6d..4b6a693565f 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -905,7 +905,7 @@ export interface IChatService { _serviceBrand: undefined; transferredSessionData: IChatTransferredSessionData | undefined; - readonly onDidSubmitRequest: Event<{ readonly chatSessionId: string }>; + readonly onDidSubmitRequest: Event<{ readonly chatSessionResource: URI }>; isEnabled(location: ChatAgentLocation): boolean; hasSessions(): boolean; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 4f291974682..f5364a34e88 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -145,7 +145,7 @@ export class ChatService extends Disposable implements IChatService { return this._transferredSessionData; } - private readonly _onDidSubmitRequest = this._register(new Emitter<{ readonly chatSessionId: string }>()); + private readonly _onDidSubmitRequest = this._register(new Emitter<{ readonly chatSessionResource: URI }>()); public readonly onDidSubmitRequest = this._onDidSubmitRequest.event; private readonly _onDidPerformUserAction = this._register(new Emitter()); @@ -1069,7 +1069,7 @@ export class ChatService extends Disposable implements IChatService { rawResponsePromise.finally(() => { this._pendingRequests.deleteAndDispose(model.sessionResource); }); - this._onDidSubmitRequest.fire({ chatSessionId: model.sessionId }); + this._onDidSubmitRequest.fire({ chatSessionResource: model.sessionResource }); return { responseCreatedPromise: responseCreated.p, responseCompletePromise: rawResponsePromise, diff --git a/src/vs/workbench/contrib/chat/common/chatTodoListService.ts b/src/vs/workbench/contrib/chat/common/chatTodoListService.ts index 425e66c6463..716f4309a49 100644 --- a/src/vs/workbench/contrib/chat/common/chatTodoListService.ts +++ b/src/vs/workbench/contrib/chat/common/chatTodoListService.ts @@ -5,9 +5,11 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; +import { URI } from '../../../../base/common/uri.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { Memento } from '../../../common/memento.js'; +import { chatSessionResourceToId } from './chatUri.js'; export interface IChatTodo { id: number; @@ -17,17 +19,17 @@ export interface IChatTodo { } export interface IChatTodoListStorage { - getTodoList(sessionId: string): IChatTodo[]; - setTodoList(sessionId: string, todoList: IChatTodo[]): void; + getTodoList(sessionResource: URI): IChatTodo[]; + setTodoList(sessionResource: URI, todoList: IChatTodo[]): void; } export const IChatTodoListService = createDecorator('chatTodoListService'); export interface IChatTodoListService { readonly _serviceBrand: undefined; - readonly onDidUpdateTodos: Event; - getTodos(sessionId: string): IChatTodo[]; - setTodos(sessionId: string, todos: IChatTodo[]): void; + readonly onDidUpdateTodos: Event; + getTodos(sessionResource: URI): IChatTodo[]; + setTodos(sessionResource: URI, todos: IChatTodo[]): void; } export class ChatTodoListStorage implements IChatTodoListStorage { @@ -37,31 +39,35 @@ export class ChatTodoListStorage implements IChatTodoListStorage { this.memento = new Memento('chat-todo-list', storageService); } - private getSessionData(sessionId: string): IChatTodo[] { + private getSessionData(sessionResource: URI): IChatTodo[] { const storage = this.memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); - return storage[sessionId] || []; + return storage[this.toKey(sessionResource)] || []; } - private setSessionData(sessionId: string, todoList: IChatTodo[]): void { + private setSessionData(sessionResource: URI, todoList: IChatTodo[]): void { const storage = this.memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); - storage[sessionId] = todoList; + storage[this.toKey(sessionResource)] = todoList; this.memento.saveMemento(); } - getTodoList(sessionId: string): IChatTodo[] { - return this.getSessionData(sessionId); + getTodoList(sessionResource: URI): IChatTodo[] { + return this.getSessionData(sessionResource); } - setTodoList(sessionId: string, todoList: IChatTodo[]): void { - this.setSessionData(sessionId, todoList); + setTodoList(sessionResource: URI, todoList: IChatTodo[]): void { + this.setSessionData(sessionResource, todoList); + } + + private toKey(sessionResource: URI): string { + return chatSessionResourceToId(sessionResource); } } export class ChatTodoListService extends Disposable implements IChatTodoListService { declare readonly _serviceBrand: undefined; - private readonly _onDidUpdateTodos = this._register(new Emitter()); - readonly onDidUpdateTodos: Event = this._onDidUpdateTodos.event; + private readonly _onDidUpdateTodos = this._register(new Emitter()); + readonly onDidUpdateTodos = this._onDidUpdateTodos.event; private todoListStorage: IChatTodoListStorage; @@ -70,12 +76,12 @@ export class ChatTodoListService extends Disposable implements IChatTodoListServ this.todoListStorage = new ChatTodoListStorage(storageService); } - getTodos(sessionId: string): IChatTodo[] { - return this.todoListStorage.getTodoList(sessionId); + getTodos(sessionResource: URI): IChatTodo[] { + return this.todoListStorage.getTodoList(sessionResource); } - setTodos(sessionId: string, todos: IChatTodo[]): void { - this.todoListStorage.setTodoList(sessionId, todos); - this._onDidUpdateTodos.fire(sessionId); + setTodos(sessionResource: URI, todos: IChatTodo[]): void { + this.todoListStorage.setTodoList(sessionResource, todos); + this._onDidUpdateTodos.fire(sessionResource); } } diff --git a/src/vs/workbench/contrib/chat/common/chatUri.ts b/src/vs/workbench/contrib/chat/common/chatUri.ts index f324137e8cd..cabd0b6d2fc 100644 --- a/src/vs/workbench/contrib/chat/common/chatUri.ts +++ b/src/vs/workbench/contrib/chat/common/chatUri.ts @@ -55,3 +55,18 @@ export namespace LocalChatSessionUri { return { chatSessionType, sessionId: new TextDecoder().decode(decodedSessionId.buffer) }; } } + +/** + * Converts a chat session resource URI to a string ID. + * + * This exists mainly for backwards compatibility with existing code that uses string IDs in telemetry and storage. + */ +export function chatSessionResourceToId(resource: URI): string { + // If we have a local session, prefer using just the id part + const localId = LocalChatSessionUri.parseLocalSessionId(resource); + if (localId) { + return localId; + } + + return resource.toString(); +} diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 63b82b743f8..ad6288fe8a8 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -63,6 +63,7 @@ export interface IChatSetCheckpointEvent { export interface IChatViewModel { readonly model: IChatModel; + /** @deprecated Use {@link sessionResource} instead */ readonly sessionId: string; readonly sessionResource: URI; readonly onDidDisposeModel: Event; @@ -263,6 +264,7 @@ export class ChatViewModel extends Disposable implements IChatViewModel { this._onDidChange.fire({ kind: 'changePlaceholder' }); } + /** @deprecated Use {@link sessionResource} instead */ get sessionId() { return this._model.sessionId; } @@ -392,6 +394,7 @@ export class ChatRequestViewModel implements IChatRequestViewModel { return this.id + `_${hash(this.variables)}_${hash(this.isComplete)}`; } + /** @deprecated */ get sessionId() { return this._model.session.sessionId; } @@ -487,6 +490,7 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi (this.isLast ? '_last' : ''); } + /** @deprecated */ get sessionId() { return this._model.session.sessionId; } diff --git a/src/vs/workbench/contrib/chat/common/tools/manageTodoListTool.ts b/src/vs/workbench/contrib/chat/common/tools/manageTodoListTool.ts index 25fb6bebeac..93ebe10fd80 100644 --- a/src/vs/workbench/contrib/chat/common/tools/manageTodoListTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/manageTodoListTool.ts @@ -21,6 +21,8 @@ import { ITelemetryService } from '../../../../../platform/telemetry/common/tele import { IChatTodo, IChatTodoListService } from '../chatTodoListService.js'; import { localize } from '../../../../../nls.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { chatSessionResourceToId, LocalChatSessionUri } from '../chatUri.js'; export const TodoListToolWriteOnlySettingId = 'chat.todoListTool.writeOnly'; export const TodoListToolDescriptionFieldSettingId = 'chat.todoListTool.descriptionField'; @@ -139,9 +141,9 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { } if (operation === 'read') { - return this.handleReadOperation(chatSessionId); + return this.handleReadOperation(LocalChatSessionUri.forSession(chatSessionId)); } else if (operation === 'write') { - return this.handleWriteOperation(args, chatSessionId); + return this.handleWriteOperation(args, LocalChatSessionUri.forSession(chatSessionId)); } else { return { content: [{ @@ -168,7 +170,7 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { const DEFAULT_TODO_SESSION_ID = 'default'; const chatSessionId = context.chatSessionId ?? args.chatSessionId ?? DEFAULT_TODO_SESSION_ID; - const currentTodoItems = this.chatTodoListService.getTodos(chatSessionId); + const currentTodoItems = this.chatTodoListService.getTodos(LocalChatSessionUri.forSession(chatSessionId)); let message: string | undefined; @@ -255,7 +257,7 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { return localize('todo.updated', "Updated todo list"); } - private handleRead(todoItems: IChatTodo[], sessionId: string): string { + private handleRead(todoItems: IChatTodo[], sessionResource: URI): string { if (todoItems.length === 0) { return 'No todo list found.'; } @@ -264,9 +266,9 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { return `# Todo List\n\n${markdownTaskList}`; } - private handleReadOperation(chatSessionId: string): IToolResult { - const todoItems = this.chatTodoListService.getTodos(chatSessionId); - const readResult = this.handleRead(todoItems, chatSessionId); + private handleReadOperation(chatSessionResource: URI): IToolResult { + const todoItems = this.chatTodoListService.getTodos(chatSessionResource); + const readResult = this.handleRead(todoItems, chatSessionResource); const statusCounts = this.calculateStatusCounts(todoItems); this.telemetryService.publicLog2( @@ -276,7 +278,7 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { notStartedCount: statusCounts.notStartedCount, inProgressCount: statusCounts.inProgressCount, completedCount: statusCounts.completedCount, - chatSessionId: chatSessionId + chatSessionId: chatSessionResourceToId(chatSessionResource) } ); @@ -288,7 +290,7 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { }; } - private handleWriteOperation(args: IManageTodoListToolInputParams, chatSessionId: string): IToolResult { + private handleWriteOperation(args: IManageTodoListToolInputParams, chatSessionResource: URI): IToolResult { if (!args.todoList) { return { content: [{ @@ -305,10 +307,10 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { status: parsedTodo.status })); - const existingTodos = this.chatTodoListService.getTodos(chatSessionId); + const existingTodos = this.chatTodoListService.getTodos(chatSessionResource); const changes = this.calculateTodoChanges(existingTodos, todoList); - this.chatTodoListService.setTodos(chatSessionId, todoList); + this.chatTodoListService.setTodos(chatSessionResource, todoList); const statusCounts = this.calculateStatusCounts(todoList); // Build warnings @@ -331,7 +333,7 @@ export class ManageTodoListTool extends Disposable implements IToolImpl { notStartedCount: statusCounts.notStartedCount, inProgressCount: statusCounts.inProgressCount, completedCount: statusCounts.completedCount, - chatSessionId: chatSessionId + chatSessionId: chatSessionResourceToId(chatSessionResource) } ); diff --git a/src/vs/workbench/contrib/chat/test/browser/chatTodoListWidget.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatTodoListWidget.test.ts index 7f6e637feaa..cddcc864909 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatTodoListWidget.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatTodoListWidget.test.ts @@ -12,6 +12,9 @@ import { mainWindow } from '../../../../../base/browser/window.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; +import { URI } from '../../../../../base/common/uri.js'; + +const testSessionUri = URI.parse('chat-session://test/session1'); suite('ChatTodoListWidget Accessibility', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); @@ -29,8 +32,8 @@ suite('ChatTodoListWidget Accessibility', () => { const mockTodoListService: IChatTodoListService = { _serviceBrand: undefined, onDidUpdateTodos: Event.None, - getTodos: (sessionId: string) => sampleTodos, - setTodos: (sessionId: string, todos: IChatTodo[]) => { } + getTodos: (sessionResource: URI) => sampleTodos, + setTodos: (sessionResource: URI, todos: IChatTodo[]) => { } }; // Mock the configuration service @@ -50,7 +53,7 @@ suite('ChatTodoListWidget Accessibility', () => { }); test('creates proper semantic list structure', () => { - widget.render('test-session'); + widget.render(testSessionUri); const todoListContainer = widget.domNode.querySelector('.todo-list-container'); assert.ok(todoListContainer, 'Should have todo list container'); @@ -68,7 +71,7 @@ suite('ChatTodoListWidget Accessibility', () => { }); test('todo items have proper accessibility attributes', () => { - widget.render('test-session'); + widget.render(testSessionUri); const todoItems = widget.domNode.querySelectorAll('.todo-item'); assert.strictEqual(todoItems.length, 3, 'Should have 3 todo items'); @@ -92,7 +95,7 @@ suite('ChatTodoListWidget Accessibility', () => { }); test('status icons are hidden from screen readers', () => { - widget.render('test-session'); + widget.render(testSessionUri); const statusIcons = widget.domNode.querySelectorAll('.todo-status-icon'); statusIcons.forEach(icon => { @@ -101,7 +104,7 @@ suite('ChatTodoListWidget Accessibility', () => { }); test('expand button has proper accessibility attributes', () => { - widget.render('test-session'); + widget.render(testSessionUri); // The expandoButton is now a Monaco Button, so we need to check its element const expandoContainer = widget.domNode.querySelector('.todo-list-expand'); @@ -123,7 +126,7 @@ suite('ChatTodoListWidget Accessibility', () => { }); test('todo items have complete aria-label with status information', () => { - widget.render('test-session'); + widget.render(testSessionUri); const todoItems = widget.domNode.querySelectorAll('.todo-item'); assert.strictEqual(todoItems.length, 3, 'Should have 3 todo items'); @@ -153,8 +156,8 @@ suite('ChatTodoListWidget Accessibility', () => { const emptyTodoListService: IChatTodoListService = { _serviceBrand: undefined, onDidUpdateTodos: Event.None, - getTodos: (sessionId: string) => [], - setTodos: (sessionId: string, todos: IChatTodo[]) => { } + getTodos: (sessionResource: URI) => [], + setTodos: (sessionResource: URI, todos: IChatTodo[]) => { } }; const emptyConfigurationService = new TestConfigurationService({ 'chat.todoListTool.descriptionField': true }); @@ -165,14 +168,14 @@ suite('ChatTodoListWidget Accessibility', () => { const emptyWidget = store.add(instantiationService.createInstance(ChatTodoListWidget)); mainWindow.document.body.appendChild(emptyWidget.domNode); - emptyWidget.render('test-session'); + emptyWidget.render(testSessionUri); // Widget should be hidden when no todos assert.strictEqual(emptyWidget.domNode.style.display, 'none', 'Widget should be hidden when no todos'); }); test('clear button has proper accessibility', () => { - widget.render('test-session'); + widget.render(testSessionUri); const clearButton = widget.domNode.querySelector('.todo-clear-button-container .monaco-button'); assert.ok(clearButton, 'Should have clear button'); @@ -180,7 +183,7 @@ suite('ChatTodoListWidget Accessibility', () => { }); test('title element displays progress correctly and is accessible', () => { - widget.render('test-session'); + widget.render(testSessionUri); const titleElement = widget.domNode.querySelector('#todo-list-title'); assert.ok(titleElement, 'Should have title element with ID'); diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index 81e52e3ca5d..fe5a80999c4 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -19,7 +19,7 @@ export class MockChatService implements IChatService { _serviceBrand: undefined; editingSessions = []; transferredSessionData: IChatTransferredSessionData | undefined; - readonly onDidSubmitRequest: Event<{ chatSessionId: string }> = Event.None; + readonly onDidSubmitRequest: Event<{ readonly chatSessionResource: URI }> = Event.None; private sessions = new ResourceMap(); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index 9fd01576fff..6f29da36b92 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -83,6 +83,7 @@ import { IInlineChatSessionService } from '../../browser/inlineChatSessionServic import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl.js'; import { CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatConfigKeys, InlineChatResponseType } from '../../common/inlineChat.js'; import { TestWorkerService } from './testWorkerService.js'; +import { URI } from '../../../../../base/common/uri.js'; suite('InlineChatController', function () { @@ -225,8 +226,8 @@ suite('InlineChatController', function () { [IChatLayoutService, new SyncDescriptor(ChatLayoutService)], [IChatTodoListService, new class extends mock() { override onDidUpdateTodos = Event.None; - override getTodos(sessionId: string): IChatTodo[] { return []; } - override setTodos(sessionId: string, todos: IChatTodo[]): void { } + override getTodos(sessionResource: URI): IChatTodo[] { return []; } + override setTodos(sessionResource: URI, todos: IChatTodo[]): void { } }], [IChatEntitlementService, new SyncDescriptor(TestChatEntitlementService)], );