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 { 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<IChatSessionsExtensionPoint[]>({
|
||||
extensionPoint: 'chatSessions',
|
||||
@@ -267,6 +270,8 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
|
||||
|
||||
private readonly _sessions = new ResourceMap<ContributedChatSessionData>();
|
||||
private readonly _editableSessions = new ResourceMap<IEditableData>();
|
||||
private readonly _registeredRequestIds = new Set<string>();
|
||||
private readonly _registeredModels = new Set<IChatModel>();
|
||||
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<boolean>) {
|
||||
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:\/\/\/(?<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;
|
||||
}
|
||||
|
||||
const renderDescriptionOnSecondRow = this.configurationService.getValue<boolean>(ChatConfiguration.ShowAgentSessionsViewDescription) && session.provider.chatSessionType !== localChatSessionType;
|
||||
const renderDescriptionOnSecondRow = this.configurationService.getValue<boolean>(ChatConfiguration.ShowAgentSessionsViewDescription);
|
||||
|
||||
if (renderDescriptionOnSecondRow && session.description) {
|
||||
templateData.container.classList.toggle('multiline', true);
|
||||
@@ -623,9 +623,9 @@ export class SessionsDelegate implements IListVirtualDelegate<ChatSessionItemWit
|
||||
|
||||
constructor(private readonly configurationService: IConfigurationService) { }
|
||||
|
||||
getHeight(element: ChatSessionItemWithProvider): number {
|
||||
getHeight(element: ChatSessionItemWithProvider | ArchivedSessionItems): number {
|
||||
// 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;
|
||||
} else {
|
||||
return SessionsDelegate.ITEM_HEIGHT;
|
||||
|
||||
@@ -14,7 +14,7 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta
|
||||
import { IEditableData } from '../../../common/views.js';
|
||||
import { IChatAgentAttachmentCapabilities, IChatAgentRequest } from './chatAgents.js';
|
||||
import { IChatEditingSession } from './chatEditingService.js';
|
||||
import { IChatRequestVariableData } from './chatModel.js';
|
||||
import { IChatModel, IChatRequestVariableData } from './chatModel.js';
|
||||
import { IChatProgress } from './chatService.js';
|
||||
|
||||
export const enum ChatSessionStatus {
|
||||
@@ -211,6 +211,7 @@ export interface IChatSessionsService {
|
||||
getEditableData(sessionResource: URI): IEditableData | undefined;
|
||||
isEditable(sessionResource: URI): boolean;
|
||||
// #endregion
|
||||
registerModelProgressListener(model: IChatModel, callback: () => void): void;
|
||||
}
|
||||
|
||||
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 { 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.');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user