mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-20 17:59:17 +00:00
* Local agent sessions provider cleanup (#279359) * add tests
This commit is contained in:
@@ -4,23 +4,22 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from '../../../../../nls.js';
|
||||
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
|
||||
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
|
||||
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { INotificationService, NeverShowAgainScope, Severity } from '../../../../../platform/notification/common/notification.js';
|
||||
import { IViewsService } from '../../../../services/views/common/viewsService.js';
|
||||
import { AGENT_SESSIONS_VIEW_ID } from './agentSessions.js';
|
||||
|
||||
const STORAGE_KEY = 'chat.closeWithActiveResponse.doNotShowAgain2';
|
||||
import { LEGACY_AGENT_SESSIONS_VIEW_ID } from '../../common/constants.js';
|
||||
import { AGENT_SESSIONS_VIEW_ID } from '../agentSessions/agentSessions.js';
|
||||
|
||||
/**
|
||||
* Shows a notification when closing a chat with an active response, informing the user
|
||||
* that the chat will continue running in the background. The notification includes a button
|
||||
* to open the Agent Sessions view and a "Don't Show Again" option.
|
||||
*/
|
||||
export function showCloseActiveChatNotification(
|
||||
accessor: ServicesAccessor
|
||||
): void {
|
||||
export function showCloseActiveChatNotification(accessor: ServicesAccessor): void {
|
||||
const notificationService = accessor.get(INotificationService);
|
||||
const viewsService = accessor.get(IViewsService);
|
||||
const configurationService = accessor.get(IConfigurationService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
|
||||
notificationService.prompt(
|
||||
Severity.Info,
|
||||
@@ -29,13 +28,18 @@ export function showCloseActiveChatNotification(
|
||||
{
|
||||
label: nls.localize('chat.openAgentSessions', "Open Agent Sessions"),
|
||||
run: async () => {
|
||||
await viewsService.openView(AGENT_SESSIONS_VIEW_ID, true);
|
||||
// TODO@bpasero remove this check once settled
|
||||
if (configurationService.getValue('chat.agentSessionsViewLocation') === 'single-view') {
|
||||
commandService.executeCommand(AGENT_SESSIONS_VIEW_ID);
|
||||
} else {
|
||||
commandService.executeCommand(LEGACY_AGENT_SESSIONS_VIEW_ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
neverShowAgain: {
|
||||
id: STORAGE_KEY,
|
||||
id: 'chat.closeWithActiveResponse.doNotShowAgain',
|
||||
scope: NeverShowAgainScope.APPLICATION
|
||||
}
|
||||
}
|
||||
@@ -2,31 +2,33 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { coalesce } from '../../../../../base/common/arrays.js';
|
||||
import { CancellationToken } from '../../../../../base/common/cancellation.js';
|
||||
import { Codicon } from '../../../../../base/common/codicons.js';
|
||||
import { Emitter, Event } from '../../../../../base/common/event.js';
|
||||
import { Emitter } from '../../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { ResourceSet } from '../../../../../base/common/map.js';
|
||||
import { localize } from '../../../../../nls.js';
|
||||
import { truncate } from '../../../../../base/common/strings.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 { IChatDetail, IChatService } 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';
|
||||
import { ChatSessionItemWithProvider } from './common.js';
|
||||
import { ChatViewId, IChatWidget, IChatWidgetService, isIChatViewViewContext } from '../chat.js';
|
||||
import { ChatSessionItemWithProvider } from '../chatSessions/common.js';
|
||||
|
||||
export class LocalAgentsSessionsProvider extends Disposable implements IChatSessionItemProvider, IWorkbenchContribution {
|
||||
|
||||
static readonly ID = 'workbench.contrib.localAgentsSessionsProvider';
|
||||
|
||||
export class LocalChatSessionsProvider extends Disposable implements IChatSessionItemProvider, IWorkbenchContribution {
|
||||
static readonly ID = 'workbench.contrib.localChatSessionsProvider';
|
||||
static readonly CHAT_WIDGET_VIEW_ID = 'workbench.panel.chat.view.copilot';
|
||||
readonly chatSessionType = localChatSessionType;
|
||||
|
||||
private readonly _onDidChange = this._register(new Emitter<void>());
|
||||
readonly onDidChange: Event<void> = this._onDidChange.event;
|
||||
readonly onDidChange = this._onDidChange.event;
|
||||
|
||||
readonly _onDidChangeChatSessionItems = this._register(new Emitter<void>());
|
||||
public get onDidChangeChatSessionItems() { return this._onDidChangeChatSessionItems.event; }
|
||||
readonly onDidChangeChatSessionItems = this._onDidChangeChatSessionItems.event;
|
||||
|
||||
constructor(
|
||||
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
|
||||
@@ -37,50 +39,52 @@ export class LocalChatSessionsProvider extends Disposable implements IChatSessio
|
||||
|
||||
this._register(this.chatSessionsService.registerChatSessionItemProvider(this));
|
||||
|
||||
this.registerWidgetListeners();
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
|
||||
// Listen for new chat widgets being added/removed
|
||||
this._register(this.chatWidgetService.onDidAddWidget(widget => {
|
||||
if (
|
||||
widget.location === ChatAgentLocation.Chat && // Only fire for chat view instance
|
||||
isIChatViewViewContext(widget.viewContext) &&
|
||||
widget.viewContext.viewId === ChatViewId
|
||||
) {
|
||||
this._onDidChange.fire();
|
||||
|
||||
this.registerWidgetModelListeners(widget);
|
||||
}
|
||||
}));
|
||||
|
||||
// Check for existing chat widgets and register listeners
|
||||
this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat)
|
||||
.filter(widget => isIChatViewViewContext(widget.viewContext) && widget.viewContext.viewId === ChatViewId)
|
||||
.forEach(widget => this.registerWidgetModelListeners(widget));
|
||||
|
||||
this._register(this.chatService.onDidDisposeSession(() => {
|
||||
this._onDidChange.fire();
|
||||
}));
|
||||
|
||||
// Listen for global session items changes for our session type
|
||||
this._register(this.chatSessionsService.onDidChangeSessionItems((sessionType) => {
|
||||
this._register(this.chatSessionsService.onDidChangeSessionItems(sessionType => {
|
||||
if (sessionType === this.chatSessionType) {
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private registerWidgetListeners(): void {
|
||||
// Listen for new chat widgets being added/removed
|
||||
this._register(this.chatWidgetService.onDidAddWidget(widget => {
|
||||
// Only fire for chat view instance
|
||||
if (widget.location === ChatAgentLocation.Chat &&
|
||||
isIChatViewViewContext(widget.viewContext) &&
|
||||
widget.viewContext.viewId === LocalChatSessionsProvider.CHAT_WIDGET_VIEW_ID) {
|
||||
this._onDidChange.fire();
|
||||
this._registerWidgetModelListeners(widget);
|
||||
}
|
||||
}));
|
||||
|
||||
// Check for existing chat widgets and register listeners
|
||||
const existingWidgets = this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat)
|
||||
.filter(widget => isIChatViewViewContext(widget.viewContext) && widget.viewContext.viewId === LocalChatSessionsProvider.CHAT_WIDGET_VIEW_ID);
|
||||
|
||||
existingWidgets.forEach(widget => {
|
||||
this._registerWidgetModelListeners(widget);
|
||||
});
|
||||
}
|
||||
|
||||
private _registerWidgetModelListeners(widget: IChatWidget): void {
|
||||
private registerWidgetModelListeners(widget: IChatWidget): void {
|
||||
const register = () => {
|
||||
this.registerModelTitleListener(widget);
|
||||
|
||||
if (widget.viewModel) {
|
||||
this.chatSessionsService.registerModelProgressListener(widget.viewModel.model, () => {
|
||||
this._onDidChangeChatSessionItems.fire();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for view model changes on this widget
|
||||
this._register(widget.onDidChangeViewModel(() => {
|
||||
register();
|
||||
@@ -93,8 +97,10 @@ export class LocalChatSessionsProvider extends Disposable implements IChatSessio
|
||||
private registerModelTitleListener(widget: IChatWidget): void {
|
||||
const model = widget.viewModel?.model;
|
||||
if (model) {
|
||||
|
||||
// Listen for model changes, specifically for title changes via setCustomTitle
|
||||
this._register(model.onDidChange((e) => {
|
||||
this._register(model.onDidChange(e => {
|
||||
|
||||
// Fire change events for all title-related changes to refresh the tree
|
||||
if (!e || e.kind === 'setCustomTitle') {
|
||||
this._onDidChange.fire();
|
||||
@@ -106,62 +112,45 @@ export class LocalChatSessionsProvider extends Disposable implements IChatSessio
|
||||
private modelToStatus(model: IChatModel): ChatSessionStatus | undefined {
|
||||
if (model.requestInProgress.get()) {
|
||||
return ChatSessionStatus.InProgress;
|
||||
} else {
|
||||
const requests = model.getRequests();
|
||||
if (requests.length > 0) {
|
||||
// Check if the last request was completed successfully or failed
|
||||
const lastRequest = requests[requests.length - 1];
|
||||
if (lastRequest?.response) {
|
||||
if (lastRequest.response.isCanceled || lastRequest.response.result?.errorDetails) {
|
||||
return ChatSessionStatus.Failed;
|
||||
} else if (lastRequest.response.isComplete) {
|
||||
return ChatSessionStatus.Completed;
|
||||
} else {
|
||||
return ChatSessionStatus.InProgress;
|
||||
}
|
||||
}
|
||||
|
||||
const requests = model.getRequests();
|
||||
if (requests.length > 0) {
|
||||
|
||||
// Check if the last request was completed successfully or failed
|
||||
const lastRequest = requests[requests.length - 1];
|
||||
if (lastRequest?.response) {
|
||||
if (lastRequest.response.isCanceled || lastRequest.response.result?.errorDetails) {
|
||||
return ChatSessionStatus.Failed;
|
||||
} else if (lastRequest.response.isComplete) {
|
||||
return ChatSessionStatus.Completed;
|
||||
} else {
|
||||
return ChatSessionStatus.InProgress;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async provideChatSessionItems(token: CancellationToken): Promise<IChatSessionItem[]> {
|
||||
const sessions: ChatSessionItemWithProvider[] = [];
|
||||
const sessionsByResource = new ResourceSet();
|
||||
this.chatService.getLiveSessionItems().forEach(sessionDetail => {
|
||||
let status: ChatSessionStatus | undefined;
|
||||
let startTime: number | undefined;
|
||||
let endTime: number | undefined;
|
||||
let description: string | undefined;
|
||||
const model = this.chatService.getSession(sessionDetail.sessionResource);
|
||||
if (model) {
|
||||
status = this.modelToStatus(model);
|
||||
startTime = model.timestamp;
|
||||
description = this.chatSessionsService.getSessionDescription(model);
|
||||
const lastResponse = model.getRequests().at(-1)?.response;
|
||||
if (lastResponse) {
|
||||
endTime = lastResponse.completedAt ?? lastResponse.timestamp;
|
||||
}
|
||||
|
||||
for (const sessionDetail of this.chatService.getLiveSessionItems()) {
|
||||
const editorSession = this.toChatSessionItem(sessionDetail);
|
||||
if (!editorSession) {
|
||||
continue;
|
||||
}
|
||||
const statistics = model ? this.getSessionStatistics(model) : undefined;
|
||||
const editorSession: ChatSessionItemWithProvider = {
|
||||
resource: sessionDetail.sessionResource,
|
||||
label: sessionDetail.title,
|
||||
iconPath: Codicon.chatSparkle,
|
||||
status,
|
||||
provider: this,
|
||||
timing: {
|
||||
startTime: startTime ?? Date.now(), // TODO@osortega this is not so good
|
||||
endTime
|
||||
},
|
||||
statistics,
|
||||
description: description || localize('chat.localSessionDescription.finished', "Finished"),
|
||||
};
|
||||
|
||||
sessionsByResource.add(sessionDetail.sessionResource);
|
||||
sessions.push(editorSession);
|
||||
});
|
||||
const history = await this.getHistoryItems();
|
||||
sessions.push(...history.filter(h => !sessionsByResource.has(h.resource)));
|
||||
}
|
||||
|
||||
if (!token.isCancellationRequested) {
|
||||
const history = await this.getHistoryItems();
|
||||
sessions.push(...history.filter(h => !sessionsByResource.has(h.resource)));
|
||||
}
|
||||
|
||||
return sessions;
|
||||
}
|
||||
@@ -169,46 +158,77 @@ export class LocalChatSessionsProvider extends Disposable implements IChatSessio
|
||||
private async getHistoryItems(): Promise<ChatSessionItemWithProvider[]> {
|
||||
try {
|
||||
const allHistory = await this.chatService.getHistorySessionItems();
|
||||
const historyItems = allHistory.map((historyDetail): ChatSessionItemWithProvider => {
|
||||
const model = this.chatService.getSession(historyDetail.sessionResource);
|
||||
const statistics = model ? this.getSessionStatistics(model) : undefined;
|
||||
return {
|
||||
resource: historyDetail.sessionResource,
|
||||
label: historyDetail.title,
|
||||
iconPath: Codicon.chatSparkle,
|
||||
provider: this,
|
||||
timing: {
|
||||
startTime: historyDetail.lastMessageDate ?? Date.now()
|
||||
},
|
||||
archived: true,
|
||||
statistics
|
||||
};
|
||||
});
|
||||
return historyItems;
|
||||
|
||||
return coalesce(allHistory.map(history => this.toChatSessionItem(history)));
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private toChatSessionItem(chat: IChatDetail): ChatSessionItemWithProvider | undefined {
|
||||
const model = this.chatService.getSession(chat.sessionResource);
|
||||
|
||||
let description: string | undefined;
|
||||
let startTime: number | undefined;
|
||||
let endTime: number | undefined;
|
||||
if (model) {
|
||||
if (!model.hasRequests) {
|
||||
return undefined; // ignore sessions without requests
|
||||
}
|
||||
|
||||
const lastResponse = model.getRequests().at(-1)?.response;
|
||||
|
||||
description = this.chatSessionsService.getSessionDescription(model);
|
||||
if (!description) {
|
||||
const responseValue = lastResponse?.response.toString();
|
||||
if (responseValue) {
|
||||
description = truncate(responseValue.replace(/\r?\n/g, ' '), 100);
|
||||
}
|
||||
}
|
||||
|
||||
startTime = model.timestamp;
|
||||
if (lastResponse) {
|
||||
endTime = lastResponse.completedAt ?? lastResponse.timestamp;
|
||||
}
|
||||
} else {
|
||||
startTime = chat.lastMessageDate;
|
||||
}
|
||||
|
||||
return {
|
||||
resource: chat.sessionResource,
|
||||
provider: this,
|
||||
label: chat.title,
|
||||
description,
|
||||
status: model ? this.modelToStatus(model) : undefined,
|
||||
iconPath: Codicon.chatSparkle,
|
||||
timing: {
|
||||
startTime,
|
||||
endTime
|
||||
},
|
||||
statistics: model ? this.getSessionStatistics(model) : undefined
|
||||
};
|
||||
}
|
||||
|
||||
private getSessionStatistics(chatModel: IChatModel) {
|
||||
let linesAdded = 0;
|
||||
let linesRemoved = 0;
|
||||
const modifiedFiles = new ResourceSet();
|
||||
const files = new ResourceSet();
|
||||
|
||||
const currentEdits = chatModel.editingSession?.entries.get();
|
||||
if (currentEdits) {
|
||||
const uncommittedEdits = currentEdits.filter((edit) => edit.state.get() === ModifiedFileEntryState.Modified);
|
||||
uncommittedEdits.forEach(edit => {
|
||||
const uncommittedEdits = currentEdits.filter(edit => edit.state.get() === ModifiedFileEntryState.Modified);
|
||||
for (const edit of uncommittedEdits) {
|
||||
linesAdded += edit.linesAdded?.get() ?? 0;
|
||||
linesRemoved += edit.linesRemoved?.get() ?? 0;
|
||||
modifiedFiles.add(edit.modifiedURI);
|
||||
});
|
||||
files.add(edit.modifiedURI);
|
||||
}
|
||||
}
|
||||
if (modifiedFiles.size === 0) {
|
||||
return;
|
||||
|
||||
if (files.size === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
files: modifiedFiles.size,
|
||||
files: files.size,
|
||||
insertions: linesAdded,
|
||||
deletions: linesRemoved,
|
||||
};
|
||||
@@ -111,7 +111,7 @@ import { ChatPasteProvidersFeature } from './chatPasteProviders.js';
|
||||
import { QuickChatService } from './chatQuick.js';
|
||||
import { ChatResponseAccessibleView } from './chatResponseAccessibleView.js';
|
||||
import { ChatTerminalOutputAccessibleView } from './chatTerminalOutputAccessibleView.js';
|
||||
import { LocalChatSessionsProvider } from './chatSessions/localChatSessionsProvider.js';
|
||||
import { LocalAgentsSessionsProvider } from './agentSessions/localAgentSessionsProvider.js';
|
||||
import { ChatSessionsView, ChatSessionsViewContrib } from './chatSessions/view/chatSessionsView.js';
|
||||
import { ChatSetupContribution, ChatTeardownContribution } from './chatSetup.js';
|
||||
import { ChatStatusBarEntry } from './chatStatus/chatStatusEntry.js';
|
||||
@@ -1145,7 +1145,7 @@ registerWorkbenchContribution2(ChatTransferContribution.ID, ChatTransferContribu
|
||||
registerWorkbenchContribution2(ChatContextContributions.ID, ChatContextContributions, WorkbenchPhase.AfterRestored);
|
||||
registerWorkbenchContribution2(ChatResponseResourceFileSystemProvider.ID, ChatResponseResourceFileSystemProvider, WorkbenchPhase.AfterRestored);
|
||||
registerWorkbenchContribution2(PromptUrlHandler.ID, PromptUrlHandler, WorkbenchPhase.BlockRestore);
|
||||
registerWorkbenchContribution2(LocalChatSessionsProvider.ID, LocalChatSessionsProvider, WorkbenchPhase.AfterRestored);
|
||||
registerWorkbenchContribution2(LocalAgentsSessionsProvider.ID, LocalAgentsSessionsProvider, WorkbenchPhase.AfterRestored);
|
||||
registerWorkbenchContribution2(ChatSessionsViewContrib.ID, ChatSessionsViewContrib, WorkbenchPhase.AfterRestored);
|
||||
registerWorkbenchContribution2(ChatSessionsView.ID, ChatSessionsView, WorkbenchPhase.BlockRestore);
|
||||
registerWorkbenchContribution2(ChatEditingNotebookFileSystemProviderContrib.ID, ChatEditingNotebookFileSystemProviderContrib, WorkbenchPhase.BlockStartup);
|
||||
|
||||
@@ -24,7 +24,7 @@ import { IChatSessionsService, localChatSessionType } from '../common/chatSessio
|
||||
import { LocalChatSessionUri } from '../common/chatUri.js';
|
||||
import { ChatAgentLocation, ChatEditorTitleMaxLength } from '../common/constants.js';
|
||||
import { IClearEditingSessionConfirmationOptions } from './actions/chatActions.js';
|
||||
import { showCloseActiveChatNotification } from './agentSessions/chatCloseNotification.js';
|
||||
import { showCloseActiveChatNotification } from './actions/chatCloseNotification.js';
|
||||
import type { IChatEditorOptions } from './chatEditor.js';
|
||||
|
||||
const ChatEditorIcon = registerIcon('chat-editor-label-icon', Codicon.chatSparkle, nls.localize('chatEditorLabelIcon', 'Icon of the chat editor label.'));
|
||||
|
||||
@@ -45,7 +45,7 @@ import { IChatWidgetService } from '../../chat.js';
|
||||
import { IChatEditorOptions } from '../../chatEditor.js';
|
||||
import { ChatSessionTracker } from '../chatSessionTracker.js';
|
||||
import { ChatSessionItemWithProvider, getSessionItemContextOverlay, NEW_CHAT_SESSION_ACTION_ID } from '../common.js';
|
||||
import { LocalChatSessionsProvider } from '../localChatSessionsProvider.js';
|
||||
import { LocalAgentsSessionsProvider } from '../../agentSessions/localAgentSessionsProvider.js';
|
||||
import { ArchivedSessionItems, GettingStartedDelegate, GettingStartedRenderer, IGettingStartedItem, SessionsDataSource, SessionsDelegate, SessionsRenderer } from './sessionsTreeRenderer.js';
|
||||
|
||||
// Identity provider for session items
|
||||
@@ -105,7 +105,7 @@ export class SessionsViewPane extends ViewPane {
|
||||
this.minimumBodySize = 44;
|
||||
|
||||
// Listen for changes in the provider if it's a LocalChatSessionsProvider
|
||||
if (provider instanceof LocalChatSessionsProvider) {
|
||||
if (provider instanceof LocalAgentsSessionsProvider) {
|
||||
this._register(provider.onDidChange(() => {
|
||||
if (this.tree && this.isBodyVisible()) {
|
||||
this.refreshTreeWithProgress();
|
||||
|
||||
@@ -37,7 +37,7 @@ import { IChatModelReference, IChatService } from '../common/chatService.js';
|
||||
import { IChatSessionsExtensionPoint, IChatSessionsService, localChatSessionType } from '../common/chatSessionsService.js';
|
||||
import { LocalChatSessionUri } from '../common/chatUri.js';
|
||||
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../common/constants.js';
|
||||
import { showCloseActiveChatNotification } from './agentSessions/chatCloseNotification.js';
|
||||
import { showCloseActiveChatNotification } from './actions/chatCloseNotification.js';
|
||||
import { ChatWidget } from './chatWidget.js';
|
||||
import { ChatViewWelcomeController, IViewWelcomeDelegate } from './viewsWelcome/chatViewWelcomeController.js';
|
||||
import { AgentSessionsControl } from './agentSessions/agentSessionsControl.js';
|
||||
|
||||
@@ -0,0 +1,780 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import assert from 'assert';
|
||||
import { CancellationToken } from '../../../../../base/common/cancellation.js';
|
||||
import { Emitter, Event } from '../../../../../base/common/event.js';
|
||||
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
|
||||
import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';
|
||||
import { IChatWidget, IChatWidgetService } from '../../browser/chat.js';
|
||||
import { LocalAgentsSessionsProvider } from '../../browser/agentSessions/localAgentSessionsProvider.js';
|
||||
import { IChatDetail, IChatService } from '../../common/chatService.js';
|
||||
import { ChatSessionStatus, IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js';
|
||||
import { ChatAgentLocation } from '../../common/constants.js';
|
||||
import { MockChatSessionsService } from '../common/mockChatSessionsService.js';
|
||||
import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js';
|
||||
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
|
||||
import { IChatModel, IChatRequestModel, IChatResponseModel } from '../../common/chatModel.js';
|
||||
import { observableValue } from '../../../../../base/common/observable.js';
|
||||
import { Codicon } from '../../../../../base/common/codicons.js';
|
||||
import { LocalChatSessionUri } from '../../common/chatUri.js';
|
||||
import { ModifiedFileEntryState } from '../../common/chatEditingService.js';
|
||||
|
||||
class MockChatWidgetService implements IChatWidgetService {
|
||||
private readonly _onDidAddWidget = new Emitter<IChatWidget>();
|
||||
readonly onDidAddWidget = this._onDidAddWidget.event;
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly lastFocusedWidget: IChatWidget | undefined;
|
||||
|
||||
private widgets: IChatWidget[] = [];
|
||||
|
||||
fireDidAddWidget(widget: IChatWidget): void {
|
||||
this._onDidAddWidget.fire(widget);
|
||||
}
|
||||
|
||||
addWidget(widget: IChatWidget): void {
|
||||
this.widgets.push(widget);
|
||||
}
|
||||
|
||||
getWidgetByInputUri(_uri: URI): IChatWidget | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getWidgetBySessionResource(_sessionResource: URI): IChatWidget | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getWidgetsByLocations(location: ChatAgentLocation): ReadonlyArray<IChatWidget> {
|
||||
return this.widgets.filter(w => w.location === location);
|
||||
}
|
||||
|
||||
revealWidget(_preserveFocus?: boolean): Promise<IChatWidget | undefined> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
reveal(_widget: IChatWidget, _preserveFocus?: boolean): Promise<boolean> {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
getAllWidgets(): ReadonlyArray<IChatWidget> {
|
||||
return this.widgets;
|
||||
}
|
||||
|
||||
openSession(_sessionResource: URI): Promise<IChatWidget | undefined> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
register(_newWidget: IChatWidget): { dispose: () => void } {
|
||||
return { dispose: () => { } };
|
||||
}
|
||||
}
|
||||
|
||||
class MockChatService implements IChatService {
|
||||
requestInProgressObs = observableValue('name', false);
|
||||
edits2Enabled: boolean = false;
|
||||
_serviceBrand: undefined;
|
||||
editingSessions = [];
|
||||
transferredSessionData = undefined;
|
||||
readonly onDidSubmitRequest = Event.None;
|
||||
|
||||
private sessions = new Map<string, IChatModel>();
|
||||
private liveSessionItems: IChatDetail[] = [];
|
||||
private historySessionItems: IChatDetail[] = [];
|
||||
|
||||
private readonly _onDidDisposeSession = new Emitter<{ sessionResource: URI; reason: 'cleared' }>();
|
||||
readonly onDidDisposeSession = this._onDidDisposeSession.event;
|
||||
|
||||
fireDidDisposeSession(sessionResource: URI): void {
|
||||
this._onDidDisposeSession.fire({ sessionResource, reason: 'cleared' });
|
||||
}
|
||||
|
||||
setLiveSessionItems(items: IChatDetail[]): void {
|
||||
this.liveSessionItems = items;
|
||||
}
|
||||
|
||||
setHistorySessionItems(items: IChatDetail[]): void {
|
||||
this.historySessionItems = items;
|
||||
}
|
||||
|
||||
addSession(sessionResource: URI, session: IChatModel): void {
|
||||
this.sessions.set(sessionResource.toString(), session);
|
||||
}
|
||||
|
||||
isEnabled(_location: ChatAgentLocation): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
hasSessions(): boolean {
|
||||
return this.sessions.size > 0;
|
||||
}
|
||||
|
||||
getProviderInfos() {
|
||||
return [];
|
||||
}
|
||||
|
||||
startSession(_location: ChatAgentLocation, _token: CancellationToken): any {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getSession(sessionResource: URI): IChatModel | undefined {
|
||||
return this.sessions.get(sessionResource.toString());
|
||||
}
|
||||
|
||||
getOrRestoreSession(_sessionResource: URI): Promise<any> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getPersistedSessionTitle(_sessionResource: URI): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
loadSessionFromContent(_data: any): any {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
loadSessionForResource(_resource: URI, _position: ChatAgentLocation, _token: CancellationToken): Promise<any> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getActiveSessionReference(_sessionResource: URI): any {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
setTitle(_sessionResource: URI, _title: string): void { }
|
||||
|
||||
appendProgress(_request: IChatRequestModel, _progress: any): void { }
|
||||
|
||||
sendRequest(_sessionResource: URI, _message: string): Promise<any> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
resendRequest(_request: IChatRequestModel, _options?: any): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
adoptRequest(_sessionResource: URI, _request: IChatRequestModel): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
removeRequest(_sessionResource: URI, _requestId: string): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
cancelCurrentRequestForSession(_sessionResource: URI): void { }
|
||||
|
||||
addCompleteRequest(): void { }
|
||||
|
||||
async getLocalSessionHistory(): Promise<IChatDetail[]> {
|
||||
return this.historySessionItems;
|
||||
}
|
||||
|
||||
async clearAllHistoryEntries(): Promise<void> { }
|
||||
|
||||
async removeHistoryEntry(_resource: URI): Promise<void> { }
|
||||
|
||||
readonly onDidPerformUserAction = Event.None;
|
||||
|
||||
notifyUserAction(_event: any): void { }
|
||||
|
||||
transferChatSession(): void { }
|
||||
|
||||
setChatSessionTitle(): void { }
|
||||
|
||||
isEditingLocation(_location: ChatAgentLocation): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getChatStorageFolder(): URI {
|
||||
return URI.file('/tmp');
|
||||
}
|
||||
|
||||
logChatIndex(): void { }
|
||||
|
||||
isPersistedSessionEmpty(_sessionResource: URI): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
activateDefaultAgent(_location: ChatAgentLocation): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
getChatSessionFromInternalUri(_sessionResource: URI): any {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getLiveSessionItems(): IChatDetail[] {
|
||||
return this.liveSessionItems;
|
||||
}
|
||||
|
||||
async getHistorySessionItems(): Promise<IChatDetail[]> {
|
||||
return this.historySessionItems;
|
||||
}
|
||||
|
||||
waitForModelDisposals(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
function createMockChatModel(options: {
|
||||
sessionResource: URI;
|
||||
hasRequests?: boolean;
|
||||
requestInProgress?: boolean;
|
||||
timestamp?: number;
|
||||
lastResponseComplete?: boolean;
|
||||
lastResponseCanceled?: boolean;
|
||||
lastResponseHasError?: boolean;
|
||||
lastResponseTimestamp?: number;
|
||||
lastResponseCompletedAt?: number;
|
||||
customTitle?: string;
|
||||
editingSession?: {
|
||||
entries: Array<{
|
||||
state: ModifiedFileEntryState;
|
||||
linesAdded: number;
|
||||
linesRemoved: number;
|
||||
modifiedURI: URI;
|
||||
}>;
|
||||
};
|
||||
}): IChatModel {
|
||||
const requests: IChatRequestModel[] = [];
|
||||
|
||||
if (options.hasRequests !== false) {
|
||||
const mockResponse: Partial<IChatResponseModel> = {
|
||||
isComplete: options.lastResponseComplete ?? true,
|
||||
isCanceled: options.lastResponseCanceled ?? false,
|
||||
result: options.lastResponseHasError ? { errorDetails: { message: 'error' } } : undefined,
|
||||
timestamp: options.lastResponseTimestamp ?? Date.now(),
|
||||
completedAt: options.lastResponseCompletedAt,
|
||||
response: {
|
||||
value: [],
|
||||
getMarkdown: () => '',
|
||||
toString: () => options.customTitle ? '' : 'Test response content'
|
||||
}
|
||||
};
|
||||
|
||||
requests.push({
|
||||
id: 'request-1',
|
||||
response: mockResponse as IChatResponseModel
|
||||
} as IChatRequestModel);
|
||||
}
|
||||
|
||||
const editingSessionEntries = options.editingSession?.entries.map(entry => ({
|
||||
state: observableValue('state', entry.state),
|
||||
linesAdded: observableValue('linesAdded', entry.linesAdded),
|
||||
linesRemoved: observableValue('linesRemoved', entry.linesRemoved),
|
||||
modifiedURI: entry.modifiedURI
|
||||
}));
|
||||
|
||||
const mockEditingSession = options.editingSession ? {
|
||||
entries: observableValue('entries', editingSessionEntries ?? [])
|
||||
} : undefined;
|
||||
|
||||
const _onDidChange = new Emitter<{ kind: string } | undefined>();
|
||||
|
||||
return {
|
||||
sessionResource: options.sessionResource,
|
||||
hasRequests: options.hasRequests !== false,
|
||||
timestamp: options.timestamp ?? Date.now(),
|
||||
requestInProgress: observableValue('requestInProgress', options.requestInProgress ?? false),
|
||||
getRequests: () => requests,
|
||||
onDidChange: _onDidChange.event,
|
||||
editingSession: mockEditingSession,
|
||||
setCustomTitle: (_title: string) => {
|
||||
_onDidChange.fire({ kind: 'setCustomTitle' });
|
||||
}
|
||||
} as unknown as IChatModel;
|
||||
}
|
||||
|
||||
suite('LocalAgentsSessionsProvider', () => {
|
||||
const disposables = new DisposableStore();
|
||||
let mockChatWidgetService: MockChatWidgetService;
|
||||
let mockChatService: MockChatService;
|
||||
let mockChatSessionsService: MockChatSessionsService;
|
||||
let instantiationService: TestInstantiationService;
|
||||
|
||||
setup(() => {
|
||||
mockChatWidgetService = new MockChatWidgetService();
|
||||
mockChatService = new MockChatService();
|
||||
mockChatSessionsService = new MockChatSessionsService();
|
||||
instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables));
|
||||
instantiationService.stub(IChatWidgetService, mockChatWidgetService);
|
||||
instantiationService.stub(IChatService, mockChatService);
|
||||
instantiationService.stub(IChatSessionsService, mockChatSessionsService);
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
disposables.clear();
|
||||
});
|
||||
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
function createProvider(): LocalAgentsSessionsProvider {
|
||||
return disposables.add(instantiationService.createInstance(LocalAgentsSessionsProvider));
|
||||
}
|
||||
|
||||
test('should have correct session type', () => {
|
||||
const provider = createProvider();
|
||||
assert.strictEqual(provider.chatSessionType, localChatSessionType);
|
||||
});
|
||||
|
||||
test('should register itself with chat sessions service', () => {
|
||||
const provider = createProvider();
|
||||
|
||||
const providers = mockChatSessionsService.getAllChatSessionItemProviders();
|
||||
assert.strictEqual(providers.length, 1);
|
||||
assert.strictEqual(providers[0], provider);
|
||||
});
|
||||
|
||||
test('should provide empty sessions when no live or history sessions', async () => {
|
||||
return runWithFakedTimers({}, async () => {
|
||||
const provider = createProvider();
|
||||
|
||||
mockChatService.setLiveSessionItems([]);
|
||||
mockChatService.setHistorySessionItems([]);
|
||||
|
||||
const sessions = await provider.provideChatSessionItems(CancellationToken.None);
|
||||
assert.strictEqual(sessions.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
test('should provide live session items', async () => {
|
||||
return runWithFakedTimers({}, async () => {
|
||||
const provider = createProvider();
|
||||
|
||||
const sessionResource = LocalChatSessionUri.forSession('test-session');
|
||||
const mockModel = createMockChatModel({
|
||||
sessionResource,
|
||||
hasRequests: true,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
mockChatService.addSession(sessionResource, mockModel);
|
||||
mockChatService.setLiveSessionItems([{
|
||||
sessionResource,
|
||||
title: 'Test Session',
|
||||
lastMessageDate: Date.now(),
|
||||
isActive: true
|
||||
}]);
|
||||
|
||||
const sessions = await provider.provideChatSessionItems(CancellationToken.None);
|
||||
assert.strictEqual(sessions.length, 1);
|
||||
assert.strictEqual(sessions[0].label, 'Test Session');
|
||||
assert.strictEqual(sessions[0].resource.toString(), sessionResource.toString());
|
||||
});
|
||||
});
|
||||
|
||||
test('should ignore sessions without requests', async () => {
|
||||
return runWithFakedTimers({}, async () => {
|
||||
const provider = createProvider();
|
||||
|
||||
const sessionResource = LocalChatSessionUri.forSession('empty-session');
|
||||
const mockModel = createMockChatModel({
|
||||
sessionResource,
|
||||
hasRequests: false
|
||||
});
|
||||
|
||||
mockChatService.addSession(sessionResource, mockModel);
|
||||
mockChatService.setLiveSessionItems([{
|
||||
sessionResource,
|
||||
title: 'Empty Session',
|
||||
lastMessageDate: Date.now(),
|
||||
isActive: true
|
||||
}]);
|
||||
|
||||
const sessions = await provider.provideChatSessionItems(CancellationToken.None);
|
||||
assert.strictEqual(sessions.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
test('should provide history session items', async () => {
|
||||
return runWithFakedTimers({}, async () => {
|
||||
const provider = createProvider();
|
||||
|
||||
const sessionResource = LocalChatSessionUri.forSession('history-session');
|
||||
|
||||
mockChatService.setLiveSessionItems([]);
|
||||
mockChatService.setHistorySessionItems([{
|
||||
sessionResource,
|
||||
title: 'History Session',
|
||||
lastMessageDate: Date.now() - 10000,
|
||||
isActive: false
|
||||
}]);
|
||||
|
||||
const sessions = await provider.provideChatSessionItems(CancellationToken.None);
|
||||
assert.strictEqual(sessions.length, 1);
|
||||
assert.strictEqual(sessions[0].label, 'History Session');
|
||||
});
|
||||
});
|
||||
|
||||
test('should not duplicate sessions in history and live', async () => {
|
||||
return runWithFakedTimers({}, async () => {
|
||||
const provider = createProvider();
|
||||
|
||||
const sessionResource = LocalChatSessionUri.forSession('duplicate-session');
|
||||
const mockModel = createMockChatModel({
|
||||
sessionResource,
|
||||
hasRequests: true
|
||||
});
|
||||
|
||||
mockChatService.addSession(sessionResource, mockModel);
|
||||
mockChatService.setLiveSessionItems([{
|
||||
sessionResource,
|
||||
title: 'Live Session',
|
||||
lastMessageDate: Date.now(),
|
||||
isActive: true
|
||||
}]);
|
||||
mockChatService.setHistorySessionItems([{
|
||||
sessionResource,
|
||||
title: 'History Session',
|
||||
lastMessageDate: Date.now() - 10000,
|
||||
isActive: false
|
||||
}]);
|
||||
|
||||
const sessions = await provider.provideChatSessionItems(CancellationToken.None);
|
||||
assert.strictEqual(sessions.length, 1);
|
||||
assert.strictEqual(sessions[0].label, 'Live Session');
|
||||
});
|
||||
});
|
||||
|
||||
suite('Session Status', () => {
|
||||
test('should return InProgress status when request in progress', async () => {
|
||||
return runWithFakedTimers({}, async () => {
|
||||
const provider = createProvider();
|
||||
|
||||
const sessionResource = LocalChatSessionUri.forSession('in-progress-session');
|
||||
const mockModel = createMockChatModel({
|
||||
sessionResource,
|
||||
hasRequests: true,
|
||||
requestInProgress: true
|
||||
});
|
||||
|
||||
mockChatService.addSession(sessionResource, mockModel);
|
||||
mockChatService.setLiveSessionItems([{
|
||||
sessionResource,
|
||||
title: 'In Progress Session',
|
||||
lastMessageDate: Date.now(),
|
||||
isActive: true
|
||||
}]);
|
||||
|
||||
const sessions = await provider.provideChatSessionItems(CancellationToken.None);
|
||||
assert.strictEqual(sessions.length, 1);
|
||||
assert.strictEqual(sessions[0].status, ChatSessionStatus.InProgress);
|
||||
});
|
||||
});
|
||||
|
||||
test('should return Completed status when last response is complete', async () => {
|
||||
return runWithFakedTimers({}, async () => {
|
||||
const provider = createProvider();
|
||||
|
||||
const sessionResource = LocalChatSessionUri.forSession('completed-session');
|
||||
const mockModel = createMockChatModel({
|
||||
sessionResource,
|
||||
hasRequests: true,
|
||||
requestInProgress: false,
|
||||
lastResponseComplete: true,
|
||||
lastResponseCanceled: false,
|
||||
lastResponseHasError: false
|
||||
});
|
||||
|
||||
mockChatService.addSession(sessionResource, mockModel);
|
||||
mockChatService.setLiveSessionItems([{
|
||||
sessionResource,
|
||||
title: 'Completed Session',
|
||||
lastMessageDate: Date.now(),
|
||||
isActive: true
|
||||
}]);
|
||||
|
||||
const sessions = await provider.provideChatSessionItems(CancellationToken.None);
|
||||
assert.strictEqual(sessions.length, 1);
|
||||
assert.strictEqual(sessions[0].status, ChatSessionStatus.Completed);
|
||||
});
|
||||
});
|
||||
|
||||
test('should return Failed status when last response was canceled', async () => {
|
||||
return runWithFakedTimers({}, async () => {
|
||||
const provider = createProvider();
|
||||
|
||||
const sessionResource = LocalChatSessionUri.forSession('canceled-session');
|
||||
const mockModel = createMockChatModel({
|
||||
sessionResource,
|
||||
hasRequests: true,
|
||||
requestInProgress: false,
|
||||
lastResponseComplete: false,
|
||||
lastResponseCanceled: true
|
||||
});
|
||||
|
||||
mockChatService.addSession(sessionResource, mockModel);
|
||||
mockChatService.setLiveSessionItems([{
|
||||
sessionResource,
|
||||
title: 'Canceled Session',
|
||||
lastMessageDate: Date.now(),
|
||||
isActive: true
|
||||
}]);
|
||||
|
||||
const sessions = await provider.provideChatSessionItems(CancellationToken.None);
|
||||
assert.strictEqual(sessions.length, 1);
|
||||
assert.strictEqual(sessions[0].status, ChatSessionStatus.Failed);
|
||||
});
|
||||
});
|
||||
|
||||
test('should return Failed status when last response has error', async () => {
|
||||
return runWithFakedTimers({}, async () => {
|
||||
const provider = createProvider();
|
||||
|
||||
const sessionResource = LocalChatSessionUri.forSession('error-session');
|
||||
const mockModel = createMockChatModel({
|
||||
sessionResource,
|
||||
hasRequests: true,
|
||||
requestInProgress: false,
|
||||
lastResponseComplete: true,
|
||||
lastResponseHasError: true
|
||||
});
|
||||
|
||||
mockChatService.addSession(sessionResource, mockModel);
|
||||
mockChatService.setLiveSessionItems([{
|
||||
sessionResource,
|
||||
title: 'Error Session',
|
||||
lastMessageDate: Date.now(),
|
||||
isActive: true
|
||||
}]);
|
||||
|
||||
const sessions = await provider.provideChatSessionItems(CancellationToken.None);
|
||||
assert.strictEqual(sessions.length, 1);
|
||||
assert.strictEqual(sessions[0].status, ChatSessionStatus.Failed);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('Session Statistics', () => {
|
||||
test('should return statistics for sessions with modified entries', async () => {
|
||||
return runWithFakedTimers({}, async () => {
|
||||
const provider = createProvider();
|
||||
|
||||
const sessionResource = LocalChatSessionUri.forSession('stats-session');
|
||||
const mockModel = createMockChatModel({
|
||||
sessionResource,
|
||||
hasRequests: true,
|
||||
editingSession: {
|
||||
entries: [
|
||||
{
|
||||
state: ModifiedFileEntryState.Modified,
|
||||
linesAdded: 10,
|
||||
linesRemoved: 5,
|
||||
modifiedURI: URI.file('/test/file1.ts')
|
||||
},
|
||||
{
|
||||
state: ModifiedFileEntryState.Modified,
|
||||
linesAdded: 20,
|
||||
linesRemoved: 3,
|
||||
modifiedURI: URI.file('/test/file2.ts')
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
mockChatService.addSession(sessionResource, mockModel);
|
||||
mockChatService.setLiveSessionItems([{
|
||||
sessionResource,
|
||||
title: 'Stats Session',
|
||||
lastMessageDate: Date.now(),
|
||||
isActive: true
|
||||
}]);
|
||||
|
||||
const sessions = await provider.provideChatSessionItems(CancellationToken.None);
|
||||
assert.strictEqual(sessions.length, 1);
|
||||
assert.ok(sessions[0].statistics);
|
||||
assert.strictEqual(sessions[0].statistics?.files, 2);
|
||||
assert.strictEqual(sessions[0].statistics?.insertions, 30);
|
||||
assert.strictEqual(sessions[0].statistics?.deletions, 8);
|
||||
});
|
||||
});
|
||||
|
||||
test('should not return statistics for sessions without modified entries', async () => {
|
||||
return runWithFakedTimers({}, async () => {
|
||||
const provider = createProvider();
|
||||
|
||||
const sessionResource = LocalChatSessionUri.forSession('no-stats-session');
|
||||
const mockModel = createMockChatModel({
|
||||
sessionResource,
|
||||
hasRequests: true,
|
||||
editingSession: {
|
||||
entries: [
|
||||
{
|
||||
state: ModifiedFileEntryState.Accepted,
|
||||
linesAdded: 10,
|
||||
linesRemoved: 5,
|
||||
modifiedURI: URI.file('/test/file1.ts')
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
mockChatService.addSession(sessionResource, mockModel);
|
||||
mockChatService.setLiveSessionItems([{
|
||||
sessionResource,
|
||||
title: 'No Stats Session',
|
||||
lastMessageDate: Date.now(),
|
||||
isActive: true
|
||||
}]);
|
||||
|
||||
const sessions = await provider.provideChatSessionItems(CancellationToken.None);
|
||||
assert.strictEqual(sessions.length, 1);
|
||||
assert.strictEqual(sessions[0].statistics, undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('Session Timing', () => {
|
||||
test('should use model timestamp for startTime when model exists', async () => {
|
||||
return runWithFakedTimers({}, async () => {
|
||||
const provider = createProvider();
|
||||
|
||||
const sessionResource = LocalChatSessionUri.forSession('timing-session');
|
||||
const modelTimestamp = Date.now() - 5000;
|
||||
const mockModel = createMockChatModel({
|
||||
sessionResource,
|
||||
hasRequests: true,
|
||||
timestamp: modelTimestamp
|
||||
});
|
||||
|
||||
mockChatService.addSession(sessionResource, mockModel);
|
||||
mockChatService.setLiveSessionItems([{
|
||||
sessionResource,
|
||||
title: 'Timing Session',
|
||||
lastMessageDate: Date.now(),
|
||||
isActive: true
|
||||
}]);
|
||||
|
||||
const sessions = await provider.provideChatSessionItems(CancellationToken.None);
|
||||
assert.strictEqual(sessions.length, 1);
|
||||
assert.strictEqual(sessions[0].timing.startTime, modelTimestamp);
|
||||
});
|
||||
});
|
||||
|
||||
test('should use lastMessageDate for startTime when model does not exist', async () => {
|
||||
return runWithFakedTimers({}, async () => {
|
||||
const provider = createProvider();
|
||||
|
||||
const sessionResource = LocalChatSessionUri.forSession('history-timing');
|
||||
const lastMessageDate = Date.now() - 10000;
|
||||
|
||||
mockChatService.setLiveSessionItems([]);
|
||||
mockChatService.setHistorySessionItems([{
|
||||
sessionResource,
|
||||
title: 'History Timing Session',
|
||||
lastMessageDate,
|
||||
isActive: false
|
||||
}]);
|
||||
|
||||
const sessions = await provider.provideChatSessionItems(CancellationToken.None);
|
||||
assert.strictEqual(sessions.length, 1);
|
||||
assert.strictEqual(sessions[0].timing.startTime, lastMessageDate);
|
||||
});
|
||||
});
|
||||
|
||||
test('should set endTime from last response completedAt', async () => {
|
||||
return runWithFakedTimers({}, async () => {
|
||||
const provider = createProvider();
|
||||
|
||||
const sessionResource = LocalChatSessionUri.forSession('endtime-session');
|
||||
const completedAt = Date.now() - 1000;
|
||||
const mockModel = createMockChatModel({
|
||||
sessionResource,
|
||||
hasRequests: true,
|
||||
lastResponseComplete: true,
|
||||
lastResponseCompletedAt: completedAt
|
||||
});
|
||||
|
||||
mockChatService.addSession(sessionResource, mockModel);
|
||||
mockChatService.setLiveSessionItems([{
|
||||
sessionResource,
|
||||
title: 'EndTime Session',
|
||||
lastMessageDate: Date.now(),
|
||||
isActive: true
|
||||
}]);
|
||||
|
||||
const sessions = await provider.provideChatSessionItems(CancellationToken.None);
|
||||
assert.strictEqual(sessions.length, 1);
|
||||
assert.strictEqual(sessions[0].timing.endTime, completedAt);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('Session Icon', () => {
|
||||
test('should use Codicon.chatSparkle as icon', async () => {
|
||||
return runWithFakedTimers({}, async () => {
|
||||
const provider = createProvider();
|
||||
|
||||
const sessionResource = LocalChatSessionUri.forSession('icon-session');
|
||||
const mockModel = createMockChatModel({
|
||||
sessionResource,
|
||||
hasRequests: true
|
||||
});
|
||||
|
||||
mockChatService.addSession(sessionResource, mockModel);
|
||||
mockChatService.setLiveSessionItems([{
|
||||
sessionResource,
|
||||
title: 'Icon Session',
|
||||
lastMessageDate: Date.now(),
|
||||
isActive: true
|
||||
}]);
|
||||
|
||||
const sessions = await provider.provideChatSessionItems(CancellationToken.None);
|
||||
assert.strictEqual(sessions.length, 1);
|
||||
assert.strictEqual(sessions[0].iconPath, Codicon.chatSparkle);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('Events', () => {
|
||||
test('should fire onDidChange when session is disposed', async () => {
|
||||
return runWithFakedTimers({}, async () => {
|
||||
const provider = createProvider();
|
||||
|
||||
let changeEventFired = false;
|
||||
disposables.add(provider.onDidChange(() => {
|
||||
changeEventFired = true;
|
||||
}));
|
||||
|
||||
const sessionResource = LocalChatSessionUri.forSession('disposed-session');
|
||||
mockChatService.fireDidDisposeSession(sessionResource);
|
||||
|
||||
assert.strictEqual(changeEventFired, true);
|
||||
});
|
||||
});
|
||||
|
||||
test('should fire onDidChange when session items change for local type', async () => {
|
||||
return runWithFakedTimers({}, async () => {
|
||||
const provider = createProvider();
|
||||
|
||||
let changeEventFired = false;
|
||||
disposables.add(provider.onDidChange(() => {
|
||||
changeEventFired = true;
|
||||
}));
|
||||
|
||||
mockChatSessionsService.notifySessionItemsChanged(localChatSessionType);
|
||||
|
||||
assert.strictEqual(changeEventFired, true);
|
||||
});
|
||||
});
|
||||
|
||||
test('should not fire onDidChange when session items change for other types', async () => {
|
||||
return runWithFakedTimers({}, async () => {
|
||||
const provider = createProvider();
|
||||
|
||||
let changeEventFired = false;
|
||||
disposables.add(provider.onDidChange(() => {
|
||||
changeEventFired = true;
|
||||
}));
|
||||
|
||||
mockChatSessionsService.notifySessionItemsChanged('other-type');
|
||||
|
||||
assert.strictEqual(changeEventFired, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -218,9 +218,10 @@ export class MockChatSessionsService implements IChatSessionsService {
|
||||
}
|
||||
|
||||
registerModelProgressListener(model: IChatModel, callback: () => void): void {
|
||||
throw new Error('Method not implemented.');
|
||||
// No-op implementation for testing
|
||||
}
|
||||
|
||||
getSessionDescription(chatModel: IChatModel): string | undefined {
|
||||
throw new Error('Method not implemented.');
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user