diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts index cfc6802edd2..e2c4d9de5f2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts @@ -37,6 +37,9 @@ import { LEGACY_AGENT_SESSIONS_VIEW_ID, ChatAgentLocation, ChatModeKind } from ' import { CHAT_CATEGORY } from './actions/chatActions.js'; import { IChatEditorOptions } from './chatEditor.js'; import { NEW_CHAT_SESSION_ACTION_ID } from './chatSessions/common.js'; +import { IChatModel, IChatProgressResponseContent, IChatRequestModel } from '../common/chatModel.js'; +import { IChatToolInvocation } from '../common/chatService.js'; +import { autorunSelfDisposable } from '../../../../base/common/observable.js'; const extensionPoint = ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'chatSessions', @@ -267,6 +270,8 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ private readonly _sessions = new ResourceMap(); private readonly _editableSessions = new ResourceMap(); + private readonly _registeredRequestIds = new Set(); + private readonly _registeredModels = new Set(); constructor( @ILogService private readonly _logService: ILogService, @@ -779,6 +784,61 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ }; } + public registerModelProgressListener(model: IChatModel, callback: () => void): void { + // Prevent duplicate registrations for the same model + if (this._registeredModels.has(model)) { + return; + } + this._registeredModels.add(model); + + // Helper function to register listeners for a request + const registerRequestListeners = (request: IChatRequestModel) => { + if (!request.response || this._registeredRequestIds.has(request.id)) { + return; + } + + this._registeredRequestIds.add(request.id); + + this._register(request.response.onDidChange(() => { + callback(); + })); + + // Track tool invocation state changes + const responseParts = request.response.response.value; + responseParts.forEach((part: IChatProgressResponseContent) => { + if (part.kind === 'toolInvocation') { + const toolInvocation = part as IChatToolInvocation; + // Use autorun to listen for state changes + this._register(autorunSelfDisposable(reader => { + const state = toolInvocation.state.read(reader); + + // Also track progress changes when executing + if (state.type === IChatToolInvocation.StateKind.Executing) { + state.progress.read(reader); + } + + callback(); + })); + } + }); + }; + // Listen for response changes on all existing requests + const requests = model.getRequests(); + requests.forEach(registerRequestListeners); + + // Listen for new requests being added + this._register(model.onDidChange(() => { + const currentRequests = model.getRequests(); + currentRequests.forEach(registerRequestListeners); + })); + + // Clean up when model is disposed + this._register(model.onDidDispose(() => { + this._registeredModels.delete(model); + })); + } + + /** * Creates a new chat session by delegating to the appropriate provider * @param chatSessionType The type of chat session provider to use diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionTracker.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionTracker.ts index 9e07bb0b363..8c9d33fdfd7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionTracker.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionTracker.ts @@ -52,6 +52,12 @@ export class ChatSessionTracker extends Disposable { const editor = e.editor as ChatEditorInput; const sessionType = editor.getSessionType(); + const model = this.chatService.getSession(editor.sessionResource!); + if (model) { + this.chatSessionsService.registerModelProgressListener(model, () => { + this.chatSessionsService.notifySessionItemsChanged(sessionType); + }); + } this.chatSessionsService.notifySessionItemsChanged(sessionType); // Emit targeted event for this session type diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/localChatSessionsProvider.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/localChatSessionsProvider.ts index ff048ef0a98..695fc5669ee 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/localChatSessionsProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/localChatSessionsProvider.ts @@ -7,11 +7,11 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../../base/common/map.js'; -import { IObservable } from '../../../../../base/common/observable.js'; +import * as nls from '../../../../../nls.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { ModifiedFileEntryState } from '../../common/chatEditingService.js'; import { IChatModel } from '../../common/chatModel.js'; -import { IChatService } from '../../common/chatService.js'; +import { IChatService, IChatToolInvocation } from '../../common/chatService.js'; import { ChatSessionStatus, IChatSessionItem, IChatSessionItemProvider, IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js'; import { ChatAgentLocation } from '../../common/constants.js'; import { IChatWidget, IChatWidgetService, isIChatViewViewContext } from '../chat.js'; @@ -76,7 +76,9 @@ export class LocalChatSessionsProvider extends Disposable implements IChatSessio const register = () => { this.registerModelTitleListener(widget); if (widget.viewModel) { - this.registerProgressListener(widget.viewModel.model.requestInProgress); + this.chatSessionsService.registerModelProgressListener(widget.viewModel.model, () => { + this._onDidChangeChatSessionItems.fire(); + }); } }; // Listen for view model changes on this widget @@ -87,12 +89,6 @@ export class LocalChatSessionsProvider extends Disposable implements IChatSessio register(); } - private registerProgressListener(observable: IObservable) { - const progressEvent = Event.fromObservableLight(observable); - this._register(progressEvent(() => { - this._onDidChangeChatSessionItems.fire(); - })); - } private registerModelTitleListener(widget: IChatWidget): void { const model = widget.viewModel?.model; @@ -147,11 +143,13 @@ export class LocalChatSessionsProvider extends Disposable implements IChatSessio } } const statistics = model ? this.getSessionStatistics(model) : undefined; + const description = model ? this.getSessionDescription(model) : undefined; const editorSession: ChatSessionItemWithProvider = { resource: sessionDetail.sessionResource, label: sessionDetail.title, iconPath: Codicon.chatSparkle, status, + description, provider: this, timing: { startTime: startTime ?? Date.now(), // TODO@osortega this is not so good @@ -206,10 +204,70 @@ export class LocalChatSessionsProvider extends Disposable implements IChatSessio modifiedFiles.add(edit.modifiedURI); }); } + if (modifiedFiles.size === 0) { + return; + } return { files: modifiedFiles.size, insertions: linesAdded, deletions: linesRemoved, }; } + + private extractFileNameFromLink(filePath: string): string { + return filePath.replace(/\[.*?\]\(file:\/\/\/(?[^)]+)\)/g, (_: string, __: string, ___: number, ____, groups?: { path?: string }) => { + const fileName = groups?.path ? groups.path.split('/').pop() || groups.path : ''; + return fileName; + }); + } + + private getSessionDescription(chatModel: IChatModel): string | undefined { + const requests = chatModel.getRequests(); + if (requests.length === 0) { + return undefined; + } + + // Get the last request to check its response status + const lastRequest = requests[requests.length - 1]; + const response = lastRequest?.response; + if (!response) { + return undefined; + } + + // If the response is complete, show Finished + if (response.isComplete) { + return nls.localize('chat.sessions.description.finished', "Finished"); + } + + // Get the response parts to find tool invocations and progress messages + const responseParts = response.response.value; + let description: string = ''; + + for (let i = responseParts.length - 1; i >= 0; i--) { + const part = responseParts[i]; + if (!description && part.kind === 'toolInvocation') { + const toolInvocation = part as IChatToolInvocation; + const state = toolInvocation.state.get(); + + if (state.type !== IChatToolInvocation.StateKind.Completed) { + const pastTenseMessage = toolInvocation.pastTenseMessage; + const invocationMessage = toolInvocation.invocationMessage; + const message = pastTenseMessage || invocationMessage; + description = typeof message === 'string' ? message : message?.value ?? ''; + + if (description) { + description = this.extractFileNameFromLink(description); + } + if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) { + const message = toolInvocation.confirmationMessages?.title && (typeof toolInvocation.confirmationMessages.title === 'string' + ? toolInvocation.confirmationMessages.title + : toolInvocation.confirmationMessages.title.value); + description = message ?? `${nls.localize('chat.sessions.description.waitingForConfirmation', "Waiting for confirmation:")} ${description}`; + } + } + } + } + + return description || nls.localize('chat.sessions.description.working', "Working..."); + } } diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts index dfbaa4b8d92..7106351bad5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts @@ -259,7 +259,7 @@ export class SessionsRenderer extends Disposable implements ITreeRenderer(ChatConfiguration.ShowAgentSessionsViewDescription) && session.provider.chatSessionType !== localChatSessionType; + const renderDescriptionOnSecondRow = this.configurationService.getValue(ChatConfiguration.ShowAgentSessionsViewDescription); if (renderDescriptionOnSecondRow && session.description) { templateData.container.classList.toggle('multiline', true); @@ -623,9 +623,9 @@ export class SessionsDelegate implements IListVirtualDelegate void): void; } export const IChatSessionsService = createDecorator('chatSessionsService'); diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts index d63564825b1..421c46e0b84 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts @@ -11,6 +11,7 @@ import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { IEditableData } from '../../../../common/views.js'; import { IChatAgentAttachmentCapabilities, IChatAgentRequest } from '../../common/chatAgents.js'; +import { IChatModel } from '../../common/chatModel.js'; import { IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemProvider, IChatSessionProviderOptionGroup, IChatSessionsExtensionPoint, IChatSessionsService, SessionOptionsChangedCallback } from '../../common/chatSessionsService.js'; export class MockChatSessionsService implements IChatSessionsService { @@ -215,4 +216,8 @@ export class MockChatSessionsService implements IChatSessionsService { getContentProviderSchemes(): string[] { return Array.from(this.contentProviders.keys()); } + + registerModelProgressListener(model: IChatModel, callback: () => void): void { + throw new Error('Method not implemented.'); + } }