mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-20 02:08:47 +00:00
Add live progress tracking to local chat session descriptions (#278474)
* Initial plan * Add live progress tracking to chat session descriptions Co-authored-by: osortega <48293249+osortega@users.noreply.github.com> * Track progress observable changes and prevent duplicate listeners Co-authored-by: osortega <48293249+osortega@users.noreply.github.com> * Enable description display for local chat sessions in progress Co-authored-by: osortega <48293249+osortega@users.noreply.github.com> * Implementation change and clean up * Refactor * Review comments * Update src/vs/workbench/contrib/chat/browser/chatSessions/localChatSessionsProvider.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/vs/workbench/contrib/chat/browser/chatSessions/localChatSessionsProvider.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Cache tool state and fix memory leak in model listener registration Co-authored-by: osortega <48293249+osortega@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: osortega <48293249+osortega@users.noreply.github.com> Co-authored-by: Osvaldo Ortega <osortega@microsoft.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -37,6 +37,9 @@ import { LEGACY_AGENT_SESSIONS_VIEW_ID, ChatAgentLocation, ChatModeKind } from '
|
|||||||
import { CHAT_CATEGORY } from './actions/chatActions.js';
|
import { CHAT_CATEGORY } from './actions/chatActions.js';
|
||||||
import { IChatEditorOptions } from './chatEditor.js';
|
import { IChatEditorOptions } from './chatEditor.js';
|
||||||
import { NEW_CHAT_SESSION_ACTION_ID } from './chatSessions/common.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<IChatSessionsExtensionPoint[]>({
|
const extensionPoint = ExtensionsRegistry.registerExtensionPoint<IChatSessionsExtensionPoint[]>({
|
||||||
extensionPoint: 'chatSessions',
|
extensionPoint: 'chatSessions',
|
||||||
@@ -267,6 +270,8 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
|
|||||||
|
|
||||||
private readonly _sessions = new ResourceMap<ContributedChatSessionData>();
|
private readonly _sessions = new ResourceMap<ContributedChatSessionData>();
|
||||||
private readonly _editableSessions = new ResourceMap<IEditableData>();
|
private readonly _editableSessions = new ResourceMap<IEditableData>();
|
||||||
|
private readonly _registeredRequestIds = new Set<string>();
|
||||||
|
private readonly _registeredModels = new Set<IChatModel>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ILogService private readonly _logService: ILogService,
|
@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
|
* Creates a new chat session by delegating to the appropriate provider
|
||||||
* @param chatSessionType The type of chat session provider to use
|
* @param chatSessionType The type of chat session provider to use
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ export class ChatSessionTracker extends Disposable {
|
|||||||
const editor = e.editor as ChatEditorInput;
|
const editor = e.editor as ChatEditorInput;
|
||||||
const sessionType = editor.getSessionType();
|
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);
|
this.chatSessionsService.notifySessionItemsChanged(sessionType);
|
||||||
|
|
||||||
// Emit targeted event for this session type
|
// Emit targeted event for this session type
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import { Codicon } from '../../../../../base/common/codicons.js';
|
|||||||
import { Emitter, Event } from '../../../../../base/common/event.js';
|
import { Emitter, Event } from '../../../../../base/common/event.js';
|
||||||
import { Disposable } from '../../../../../base/common/lifecycle.js';
|
import { Disposable } from '../../../../../base/common/lifecycle.js';
|
||||||
import { ResourceSet } from '../../../../../base/common/map.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 { IWorkbenchContribution } from '../../../../common/contributions.js';
|
||||||
import { ModifiedFileEntryState } from '../../common/chatEditingService.js';
|
import { ModifiedFileEntryState } from '../../common/chatEditingService.js';
|
||||||
import { IChatModel } from '../../common/chatModel.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 { ChatSessionStatus, IChatSessionItem, IChatSessionItemProvider, IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js';
|
||||||
import { ChatAgentLocation } from '../../common/constants.js';
|
import { ChatAgentLocation } from '../../common/constants.js';
|
||||||
import { IChatWidget, IChatWidgetService, isIChatViewViewContext } from '../chat.js';
|
import { IChatWidget, IChatWidgetService, isIChatViewViewContext } from '../chat.js';
|
||||||
@@ -76,7 +76,9 @@ export class LocalChatSessionsProvider extends Disposable implements IChatSessio
|
|||||||
const register = () => {
|
const register = () => {
|
||||||
this.registerModelTitleListener(widget);
|
this.registerModelTitleListener(widget);
|
||||||
if (widget.viewModel) {
|
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
|
// Listen for view model changes on this widget
|
||||||
@@ -87,12 +89,6 @@ export class LocalChatSessionsProvider extends Disposable implements IChatSessio
|
|||||||
|
|
||||||
register();
|
register();
|
||||||
}
|
}
|
||||||
private registerProgressListener(observable: IObservable<boolean>) {
|
|
||||||
const progressEvent = Event.fromObservableLight(observable);
|
|
||||||
this._register(progressEvent(() => {
|
|
||||||
this._onDidChangeChatSessionItems.fire();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerModelTitleListener(widget: IChatWidget): void {
|
private registerModelTitleListener(widget: IChatWidget): void {
|
||||||
const model = widget.viewModel?.model;
|
const model = widget.viewModel?.model;
|
||||||
@@ -147,11 +143,13 @@ export class LocalChatSessionsProvider extends Disposable implements IChatSessio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const statistics = model ? this.getSessionStatistics(model) : undefined;
|
const statistics = model ? this.getSessionStatistics(model) : undefined;
|
||||||
|
const description = model ? this.getSessionDescription(model) : undefined;
|
||||||
const editorSession: ChatSessionItemWithProvider = {
|
const editorSession: ChatSessionItemWithProvider = {
|
||||||
resource: sessionDetail.sessionResource,
|
resource: sessionDetail.sessionResource,
|
||||||
label: sessionDetail.title,
|
label: sessionDetail.title,
|
||||||
iconPath: Codicon.chatSparkle,
|
iconPath: Codicon.chatSparkle,
|
||||||
status,
|
status,
|
||||||
|
description,
|
||||||
provider: this,
|
provider: this,
|
||||||
timing: {
|
timing: {
|
||||||
startTime: startTime ?? Date.now(), // TODO@osortega this is not so good
|
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);
|
modifiedFiles.add(edit.modifiedURI);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (modifiedFiles.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
files: modifiedFiles.size,
|
files: modifiedFiles.size,
|
||||||
insertions: linesAdded,
|
insertions: linesAdded,
|
||||||
deletions: linesRemoved,
|
deletions: linesRemoved,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extractFileNameFromLink(filePath: string): string {
|
||||||
|
return filePath.replace(/\[.*?\]\(file:\/\/\/(?<path>[^)]+)\)/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...");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ export class SessionsRenderer extends Disposable implements ITreeRenderer<IChatS
|
|||||||
iconTheme = session.iconPath;
|
iconTheme = session.iconPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderDescriptionOnSecondRow = this.configurationService.getValue<boolean>(ChatConfiguration.ShowAgentSessionsViewDescription) && session.provider.chatSessionType !== localChatSessionType;
|
const renderDescriptionOnSecondRow = this.configurationService.getValue<boolean>(ChatConfiguration.ShowAgentSessionsViewDescription);
|
||||||
|
|
||||||
if (renderDescriptionOnSecondRow && session.description) {
|
if (renderDescriptionOnSecondRow && session.description) {
|
||||||
templateData.container.classList.toggle('multiline', true);
|
templateData.container.classList.toggle('multiline', true);
|
||||||
@@ -623,9 +623,9 @@ export class SessionsDelegate implements IListVirtualDelegate<ChatSessionItemWit
|
|||||||
|
|
||||||
constructor(private readonly configurationService: IConfigurationService) { }
|
constructor(private readonly configurationService: IConfigurationService) { }
|
||||||
|
|
||||||
getHeight(element: ChatSessionItemWithProvider): number {
|
getHeight(element: ChatSessionItemWithProvider | ArchivedSessionItems): number {
|
||||||
// Return consistent height for all items (single-line layout)
|
// Return consistent height for all items (single-line layout)
|
||||||
if (element.description && this.configurationService.getValue(ChatConfiguration.ShowAgentSessionsViewDescription) && element.provider.chatSessionType !== localChatSessionType) {
|
if (this.configurationService.getValue(ChatConfiguration.ShowAgentSessionsViewDescription) && !(element instanceof ArchivedSessionItems) && element.description) {
|
||||||
return SessionsDelegate.ITEM_HEIGHT_WITH_DESCRIPTION;
|
return SessionsDelegate.ITEM_HEIGHT_WITH_DESCRIPTION;
|
||||||
} else {
|
} else {
|
||||||
return SessionsDelegate.ITEM_HEIGHT;
|
return SessionsDelegate.ITEM_HEIGHT;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta
|
|||||||
import { IEditableData } from '../../../common/views.js';
|
import { IEditableData } from '../../../common/views.js';
|
||||||
import { IChatAgentAttachmentCapabilities, IChatAgentRequest } from './chatAgents.js';
|
import { IChatAgentAttachmentCapabilities, IChatAgentRequest } from './chatAgents.js';
|
||||||
import { IChatEditingSession } from './chatEditingService.js';
|
import { IChatEditingSession } from './chatEditingService.js';
|
||||||
import { IChatRequestVariableData } from './chatModel.js';
|
import { IChatModel, IChatRequestVariableData } from './chatModel.js';
|
||||||
import { IChatProgress } from './chatService.js';
|
import { IChatProgress } from './chatService.js';
|
||||||
|
|
||||||
export const enum ChatSessionStatus {
|
export const enum ChatSessionStatus {
|
||||||
@@ -211,6 +211,7 @@ export interface IChatSessionsService {
|
|||||||
getEditableData(sessionResource: URI): IEditableData | undefined;
|
getEditableData(sessionResource: URI): IEditableData | undefined;
|
||||||
isEditable(sessionResource: URI): boolean;
|
isEditable(sessionResource: URI): boolean;
|
||||||
// #endregion
|
// #endregion
|
||||||
|
registerModelProgressListener(model: IChatModel, callback: () => void): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IChatSessionsService = createDecorator<IChatSessionsService>('chatSessionsService');
|
export const IChatSessionsService = createDecorator<IChatSessionsService>('chatSessionsService');
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { ThemeIcon } from '../../../../../base/common/themables.js';
|
|||||||
import { URI } from '../../../../../base/common/uri.js';
|
import { URI } from '../../../../../base/common/uri.js';
|
||||||
import { IEditableData } from '../../../../common/views.js';
|
import { IEditableData } from '../../../../common/views.js';
|
||||||
import { IChatAgentAttachmentCapabilities, IChatAgentRequest } from '../../common/chatAgents.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';
|
import { IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemProvider, IChatSessionProviderOptionGroup, IChatSessionsExtensionPoint, IChatSessionsService, SessionOptionsChangedCallback } from '../../common/chatSessionsService.js';
|
||||||
|
|
||||||
export class MockChatSessionsService implements IChatSessionsService {
|
export class MockChatSessionsService implements IChatSessionsService {
|
||||||
@@ -215,4 +216,8 @@ export class MockChatSessionsService implements IChatSessionsService {
|
|||||||
getContentProviderSchemes(): string[] {
|
getContentProviderSchemes(): string[] {
|
||||||
return Array.from(this.contentProviders.keys());
|
return Array.from(this.contentProviders.keys());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerModelProgressListener(model: IChatModel, callback: () => void): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user