diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 985ef0148ab..5024d5b8ab3 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -223,6 +223,8 @@ export class MenuId { static readonly TimelineTitle = new MenuId('TimelineTitle'); static readonly TimelineTitleContext = new MenuId('TimelineTitleContext'); static readonly TimelineFilterSubMenu = new MenuId('TimelineFilterSubMenu'); + static readonly AgentSessionsTitle = new MenuId('AgentSessionsTitle'); + static readonly AgentSessionsFilterSubMenu = new MenuId('AgentSessionsFilterSubMenu'); static readonly AccountsContext = new MenuId('AccountsContext'); static readonly SidebarTitle = new MenuId('SidebarTitle'); static readonly PanelTitle = new MenuId('PanelTitle'); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts index 5e83a16c45f..1488d1f352a 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts @@ -12,7 +12,7 @@ import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/cont import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js'; import { Codicon } from '../../../../../base/common/codicons.js'; -import { AgentSessionProviders } from '../agentSessions/agentSessions.js'; +import { AgentSessionProviders, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../agentSessions/agentSessions.js'; import { localize, localize2 } from '../../../../../nls.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { basename, relativePath } from '../../../../../base/common/resources.js'; @@ -89,13 +89,13 @@ export class ChatContinueInSessionActionItem extends ActionWidgetDropdownActionV // Continue in Background const backgroundContrib = contributions.find(contrib => contrib.type === AgentSessionProviders.Background); if (backgroundContrib && backgroundContrib.canDelegate !== false) { - actions.push(this.toAction(backgroundContrib, instantiationService)); + actions.push(this.toAction(AgentSessionProviders.Background, backgroundContrib, instantiationService)); } // Continue in Cloud const cloudContrib = contributions.find(contrib => contrib.type === AgentSessionProviders.Cloud); if (cloudContrib && cloudContrib.canDelegate !== false) { - actions.push(this.toAction(cloudContrib, instantiationService)); + actions.push(this.toAction(AgentSessionProviders.Cloud, cloudContrib, instantiationService)); } // Offer actions to enter setup if we have no contributions @@ -109,32 +109,26 @@ export class ChatContinueInSessionActionItem extends ActionWidgetDropdownActionV }; } - private static toAction(contrib: IChatSessionsExtensionPoint, instantiationService: IInstantiationService): IActionWidgetDropdownAction { + private static toAction(provider: AgentSessionProviders, contrib: IChatSessionsExtensionPoint, instantiationService: IInstantiationService): IActionWidgetDropdownAction { return { id: contrib.type, enabled: true, - icon: contrib.type === AgentSessionProviders.Cloud ? Codicon.cloud : Codicon.collection, + icon: getAgentSessionProviderIcon(provider), class: undefined, + label: localize('continueSessionIn', "Continue in {0}", getAgentSessionProviderName(provider)), tooltip: contrib.displayName, - label: contrib.type === AgentSessionProviders.Cloud ? - localize('continueInCloud', "Continue in Cloud") : - localize('continueInBackground', "Continue in Background"), run: () => instantiationService.invokeFunction(accessor => new CreateRemoteAgentJobAction().run(accessor, contrib)) }; } - private static toSetupAction(type: string, instantiationService: IInstantiationService): IActionWidgetDropdownAction { - const label = type === AgentSessionProviders.Cloud ? - localize('continueInCloud', "Continue in Cloud") : - localize('continueInBackground', "Continue in Background"); - + private static toSetupAction(provider: AgentSessionProviders, instantiationService: IInstantiationService): IActionWidgetDropdownAction { return { - id: type, + id: provider, enabled: true, - icon: type === AgentSessionProviders.Cloud ? Codicon.cloud : Codicon.collection, + icon: getAgentSessionProviderIcon(provider), class: undefined, - tooltip: label, - label, + label: localize('continueSessionIn', "Continue in {0}", getAgentSessionProviderName(provider)), + tooltip: localize('continueSessionIn', "Continue in {0}", getAgentSessionProviderName(provider)), run: () => instantiationService.invokeFunction(accessor => { const commandService = accessor.get(ICommandService); return commandService.executeCommand(CHAT_SETUP_ACTION_ID); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionViewModel.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionViewModel.ts index 5d437a8eeeb..1499facff9c 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionViewModel.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionViewModel.ts @@ -12,9 +12,12 @@ import { Disposable } from '../../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; +import { MenuId } from '../../../../../platform/actions/common/actions.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; import { ChatSessionStatus, IChatSessionItemProvider, IChatSessionsExtensionPoint, IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js'; -import { AgentSessionProviders } from './agentSessions.js'; +import { AgentSessionProviders, getAgentSessionProviderIcon, getAgentSessionProviderName } from './agentSessions.js'; +import { AgentSessionsViewFilter } from './agentSessionsViewFilter.js'; //#region Interfaces, Types @@ -74,9 +77,11 @@ export function isAgentSessionsViewModel(obj: IAgentSessionsViewModel | IAgentSe //#endregion -export class AgentSessionsViewModel extends Disposable implements IAgentSessionsViewModel { +export interface IAgentSessionsViewModelOptions { + readonly filterMenuId: MenuId; +} - readonly sessions: IAgentSessionViewModel[] = []; +export class AgentSessionsViewModel extends Disposable implements IAgentSessionsViewModel { private readonly _onWillResolve = this._register(new Emitter()); readonly onWillResolve = this._onWillResolve.event; @@ -87,15 +92,27 @@ export class AgentSessionsViewModel extends Disposable implements IAgentSessions private readonly _onDidChangeSessions = this._register(new Emitter()); readonly onDidChangeSessions = this._onDidChangeSessions.event; + private _sessions: IAgentSessionViewModel[] = []; + + get sessions(): IAgentSessionViewModel[] { + return this._sessions.filter(session => !this.filter.excludes.has(session.provider.chatSessionType)); + } + private readonly resolver = this._register(new ThrottledDelayer(100)); private readonly providersToResolve = new Set(); + private readonly filter: AgentSessionsViewFilter; + constructor( + options: IAgentSessionsViewModelOptions, @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, @ILifecycleService private readonly lifecycleService: ILifecycleService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); + this.filter = this._register(this.instantiationService.createInstance(AgentSessionsViewFilter, { filterMenuId: options.filterMenuId })); + this.registerListeners(); this.resolve(undefined); @@ -105,6 +122,7 @@ export class AgentSessionsViewModel extends Disposable implements IAgentSessions this._register(this.chatSessionsService.onDidChangeItemsProviders(({ chatSessionType: provider }) => this.resolve(provider))); this._register(this.chatSessionsService.onDidChangeAvailability(() => this.resolve(undefined))); this._register(this.chatSessionsService.onDidChangeSessionItems(provider => this.resolve(provider))); + this._register(this.filter.onDidChange(() => this._onDidChangeSessions.fire())); } async resolve(provider: string | string[] | undefined): Promise { @@ -142,7 +160,7 @@ export class AgentSessionsViewModel extends Disposable implements IAgentSessions const newSessions: IAgentSessionViewModel[] = []; for (const provider of this.chatSessionsService.getAllChatSessionItemProviders()) { if (!providersToResolve.includes(undefined) && !providersToResolve.includes(provider.chatSessionType)) { - newSessions.push(...this.sessions.filter(session => session.provider.chatSessionType === provider.chatSessionType)); + newSessions.push(...this._sessions.filter(session => session.provider.chatSessionType === provider.chatSessionType)); continue; // skipped for resolving, preserve existing ones } @@ -172,17 +190,17 @@ export class AgentSessionsViewModel extends Disposable implements IAgentSessions let icon: ThemeIcon; let providerLabel: string; switch ((provider.chatSessionType)) { - case localChatSessionType: - providerLabel = localize('chat.session.providerLabel.local', "Local"); - icon = Codicon.vm; + case AgentSessionProviders.Local: + providerLabel = getAgentSessionProviderName(AgentSessionProviders.Local); + icon = getAgentSessionProviderIcon(AgentSessionProviders.Local); break; case AgentSessionProviders.Background: - providerLabel = localize('chat.session.providerLabel.background', "Background"); - icon = Codicon.collection; + providerLabel = getAgentSessionProviderName(AgentSessionProviders.Background); + icon = getAgentSessionProviderIcon(AgentSessionProviders.Background); break; case AgentSessionProviders.Cloud: - providerLabel = localize('chat.session.providerLabel.cloud', "Cloud"); - icon = Codicon.cloud; + providerLabel = getAgentSessionProviderName(AgentSessionProviders.Cloud); + icon = getAgentSessionProviderIcon(AgentSessionProviders.Cloud); break; default: { providerLabel = mapSessionContributionToType.get(provider.chatSessionType)?.name ?? provider.chatSessionType; @@ -208,8 +226,8 @@ export class AgentSessionsViewModel extends Disposable implements IAgentSessions } } - this.sessions.length = 0; - this.sessions.push(...newSessions); + this._sessions.length = 0; + this._sessions.push(...newSessions); this._onDidChangeSessions.fire(); } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts index deb1d3b7507..c161abfb023 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts @@ -3,6 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from '../../../../../nls.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; import { localChatSessionType } from '../../common/chatSessionsService.js'; export const AGENT_SESSIONS_VIEW_CONTAINER_ID = 'workbench.viewContainer.agentSessions'; @@ -13,3 +16,26 @@ export enum AgentSessionProviders { Background = 'copilotcli', Cloud = 'copilot-cloud-agent', } + +export function getAgentSessionProviderName(provider: AgentSessionProviders): string { + switch (provider) { + case AgentSessionProviders.Local: + return localize('chat.session.providerLabel.local', "Local"); + case AgentSessionProviders.Background: + return localize('chat.session.providerLabel.background', "Background"); + case AgentSessionProviders.Cloud: + return localize('chat.session.providerLabel.cloud', "Cloud"); + } +} + +export function getAgentSessionProviderIcon(provider: AgentSessionProviders): ThemeIcon { + switch (provider) { + case AgentSessionProviders.Local: + return Codicon.vm; + case AgentSessionProviders.Background: + return Codicon.collection; + case AgentSessionProviders.Cloud: + return Codicon.cloud; + } +} + diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts index 5a5b4b8cf91..ffa4dba960c 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsActions.ts @@ -4,13 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import './media/agentsessionsactions.css'; -import { localize } from '../../../../../nls.js'; +import { localize, localize2 } from '../../../../../nls.js'; import { IAgentSessionViewModel } from './agentSessionViewModel.js'; import { Action, IAction } from '../../../../../base/common/actions.js'; import { ActionViewItem, IActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { EventHelper, h, hide, show } from '../../../../../base/browser/dom.js'; import { assertReturnsDefined } from '../../../../../base/common/types.js'; +import { ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; +import { ViewAction } from '../../../../browser/parts/views/viewPane.js'; +import { AGENT_SESSIONS_VIEW_ID } from './agentSessions.js'; +import { AgentSessionsView } from './agentSessionsView.js'; //#region Diff Statistics Action @@ -102,3 +108,53 @@ export class AgentSessionDiffActionViewItem extends ActionViewItem { } //#endregion + +//#region View Actions + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'agentSessionsView.refresh', + title: localize2('refresh', "Refresh Agent Sessions"), + icon: Codicon.refresh, + menu: { + id: MenuId.AgentSessionsTitle, + group: 'navigation', + order: 1 + }, + viewId: AGENT_SESSIONS_VIEW_ID + }); + } + runInView(accessor: ServicesAccessor, view: AgentSessionsView): void { + view.refresh(); + } +}); + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'agentSessionsView.find', + title: localize2('find', "Find Agent Session"), + icon: Codicon.search, + menu: { + id: MenuId.AgentSessionsTitle, + group: 'navigation', + order: 2 + }, + viewId: AGENT_SESSIONS_VIEW_ID + }); + } + runInView(accessor: ServicesAccessor, view: AgentSessionsView): void { + view.openFind(); + } +}); + +MenuRegistry.appendMenuItem(MenuId.AgentSessionsTitle, { + submenu: MenuId.AgentSessionsFilterSubMenu, + title: localize('filterAgentSessions', "Filter Agent Sessions"), + group: 'navigation', + order: 100, + icon: Codicon.filter +} satisfies ISubmenuItem); + +//#endregion diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsView.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsView.ts index afe4b66dced..9f5e4449a4b 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsView.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsView.ts @@ -10,7 +10,7 @@ import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/cont import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; import { registerIcon } from '../../../../../platform/theme/common/iconRegistry.js'; -import { IViewPaneOptions, ViewAction, ViewPane } from '../../../../browser/parts/views/viewPane.js'; +import { IViewPaneOptions, ViewPane } from '../../../../browser/parts/views/viewPane.js'; import { ViewPaneContainer } from '../../../../browser/parts/views/viewPaneContainer.js'; import { IViewContainersRegistry, Extensions as ViewExtensions, ViewContainerLocation, IViewsRegistry, IViewDescriptor, IViewDescriptorService } from '../../../../common/views.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; @@ -18,7 +18,7 @@ import { ChatConfiguration } from '../../common/constants.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; -import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; @@ -30,7 +30,7 @@ import { defaultButtonStyles } from '../../../../../platform/theme/browser/defau import { ButtonWithDropdown } from '../../../../../base/browser/ui/button/button.js'; import { IAction, Separator, toAction } from '../../../../../base/common/actions.js'; import { FuzzyScore } from '../../../../../base/common/filters.js'; -import { IMenuService, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; import { IChatSessionsService } from '../../common/chatSessionsService.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { getSessionItemContextOverlay, NEW_CHAT_SESSION_ACTION_ID } from '../chatSessions/common.js'; @@ -74,9 +74,7 @@ export class AgentSessionsView extends ViewPane { @IMenuService private readonly menuService: IMenuService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); - - this.registerActions(); + super({ ...options, titleMenuId: MenuId.AgentSessionsTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); } protected override renderBody(container: HTMLElement): void { @@ -94,9 +92,9 @@ export class AgentSessionsView extends ViewPane { } private registerListeners(): void { - const list = assertReturnsDefined(this.list); // Sessions List + const list = assertReturnsDefined(this.list); this._register(this.onDidChangeBodyVisibility(visible => { if (!visible || this.sessionsViewModel) { return; @@ -124,7 +122,7 @@ export class AgentSessionsView extends ViewPane { })); } - private async openAgentSession(e: IOpenEvent) { + private async openAgentSession(e: IOpenEvent): Promise { const session = e.element; if (!session) { return; @@ -172,48 +170,7 @@ export class AgentSessionsView extends ViewPane { menu.dispose(); } - private registerActions(): void { - - this._register(registerAction2(class extends ViewAction { - constructor() { - super({ - id: 'agentSessionsView.refresh', - title: localize2('refresh', "Refresh Agent Sessions"), - icon: Codicon.refresh, - menu: { - id: MenuId.ViewTitle, - when: ContextKeyExpr.equals('view', AGENT_SESSIONS_VIEW_ID), - group: 'navigation', - order: 1 - }, - viewId: AGENT_SESSIONS_VIEW_ID - }); - } - runInView(accessor: ServicesAccessor, view: AgentSessionsView): void { - view.sessionsViewModel?.resolve(undefined); - } - })); - - this._register(registerAction2(class extends ViewAction { - constructor() { - super({ - id: 'agentSessionsView.find', - title: localize2('find', "Find Agent Session"), - icon: Codicon.search, - menu: { - id: MenuId.ViewTitle, - when: ContextKeyExpr.equals('view', AGENT_SESSIONS_VIEW_ID), - group: 'navigation', - order: 2 - }, - viewId: AGENT_SESSIONS_VIEW_ID - }); - } - runInView(accessor: ServicesAccessor, view: AgentSessionsView): void { - view.list?.openFind(); - } - })); - } + //#endregion //#region New Session Controls @@ -343,7 +300,7 @@ export class AgentSessionsView extends ViewPane { } private createViewModel(): void { - const sessionsViewModel = this.sessionsViewModel = this._register(this.instantiationService.createInstance(AgentSessionsViewModel)); + const sessionsViewModel = this.sessionsViewModel = this._register(this.instantiationService.createInstance(AgentSessionsViewModel, { filterMenuId: MenuId.AgentSessionsFilterSubMenu })); this.list?.setInput(sessionsViewModel); this._register(sessionsViewModel.onDidChangeSessions(() => { @@ -370,6 +327,18 @@ export class AgentSessionsView extends ViewPane { //#endregion + //#region Actions internal API + + openFind(): void { + this.list?.openFind(); + } + + refresh(): void { + this.sessionsViewModel?.resolve(undefined); + } + + //#endregion + protected override layoutBody(height: number, width: number): void { super.layoutBody(height, width); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewFilter.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewFilter.ts new file mode 100644 index 00000000000..6ecbfc1cb99 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewFilter.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from '../../../../../base/common/event.js'; +import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { escapeRegExpCharacters } from '../../../../../base/common/strings.js'; +import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; +import { registerAction2, Action2, MenuId } from '../../../../../platform/actions/common/actions.js'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +import { IChatSessionsService } from '../../common/chatSessionsService.js'; +import { AgentSessionProviders, getAgentSessionProviderName } from './agentSessions.js'; + +export interface IAgentSessionsViewFilterOptions { + readonly filterMenuId: MenuId; +} + +export class AgentSessionsViewFilter extends Disposable { + + private static readonly STORAGE_KEY = 'agentSessions.filter.excludes'; + private static readonly CONTEXT_KEY = 'agentSessionsFilterExcludes'; + + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + + private _excludes = new Set(); + get excludes(): Set { return this._excludes; } + + private excludesContext: IContextKey; + + private actionDisposables = this._register(new DisposableStore()); + + constructor( + private readonly options: IAgentSessionsViewFilterOptions, + @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, + @IStorageService private readonly storageService: IStorageService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + ) { + super(); + + this.excludesContext = new RawContextKey(AgentSessionsViewFilter.CONTEXT_KEY, '[]', true).bindTo(this.contextKeyService); + + this.updateExcludes(false); + + this.registerListeners(); + } + + private registerListeners(): void { + this._register(this.chatSessionsService.onDidChangeItemsProviders(() => this.updateFilterActions())); + this._register(this.chatSessionsService.onDidChangeAvailability(() => this.updateFilterActions())); + + this._register(this.storageService.onDidChangeValue(StorageScope.PROFILE, AgentSessionsViewFilter.STORAGE_KEY, this._store)(() => this.updateExcludes(true))); + } + + private updateExcludes(fromEvent: boolean): void { + const excludedTypesString = this.storageService.get(AgentSessionsViewFilter.STORAGE_KEY, StorageScope.PROFILE, '[]'); + this.excludesContext.set(excludedTypesString); + this._excludes = new Set(JSON.parse(excludedTypesString)); + + if (fromEvent) { + this._onDidChange.fire(); + } + } + + private updateFilterActions(): void { + this.actionDisposables.clear(); + + const providers: { id: string; label: string }[] = [ + { id: AgentSessionProviders.Local, label: getAgentSessionProviderName(AgentSessionProviders.Local) }, + { id: AgentSessionProviders.Background, label: getAgentSessionProviderName(AgentSessionProviders.Background) }, + { id: AgentSessionProviders.Cloud, label: getAgentSessionProviderName(AgentSessionProviders.Cloud) }, + ]; + + for (const provider of this.chatSessionsService.getAllChatSessionContributions()) { + if (providers.find(p => p.id === provider.type)) { + continue; // already added + } + + providers.push({ id: provider.type, label: provider.name }); + } + + const that = this; + let counter = 0; + for (const provider of providers) { + this.actionDisposables.add(registerAction2(class extends Action2 { + constructor() { + super({ + id: `agentSessions.filter.toggleExclude:${provider.id}`, + title: provider.label, + menu: { + id: that.options.filterMenuId, + group: 'navigation', + order: counter++, + }, + toggled: ContextKeyExpr.regex(AgentSessionsViewFilter.CONTEXT_KEY, new RegExp(`\\b${escapeRegExpCharacters(provider.id)}\\b`)).negate() + }); + } + run(accessor: ServicesAccessor): void { + const excludes = new Set(that._excludes); + if (excludes.has(provider.id)) { + excludes.delete(provider.id); + } else { + excludes.add(provider.id); + } + + const storageService = accessor.get(IStorageService); + storageService.store(AgentSessionsViewFilter.STORAGE_KEY, JSON.stringify([...excludes]), StorageScope.PROFILE, StorageTarget.USER); + } + })); + } + } +} diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts index 5fe2440f7e5..0c2665473bb 100644 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts @@ -11,12 +11,15 @@ import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { AgentSessionsViewModel, IAgentSessionViewModel, isAgentSession, isAgentSessionsViewModel, isLocalAgentSessionItem } from '../../browser/agentSessions/agentSessionViewModel.js'; -import { ChatSessionStatus, IChatSessionItem, IChatSessionItemProvider, localChatSessionType } from '../../common/chatSessionsService.js'; +import { ChatSessionStatus, IChatSessionItem, IChatSessionItemProvider, IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js'; import { LocalChatSessionUri } from '../../common/chatUri.js'; import { MockChatSessionsService } from '../common/mockChatSessionsService.js'; -import { TestLifecycleService } from '../../../../test/browser/workbenchTestServices.js'; +import { TestLifecycleService, workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js'; import { Codicon } from '../../../../../base/common/codicons.js'; +import { MenuId } from '../../../../../platform/actions/common/actions.js'; +import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; +import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; suite('AgentSessionsViewModel', () => { @@ -24,10 +27,21 @@ suite('AgentSessionsViewModel', () => { let mockChatSessionsService: MockChatSessionsService; let mockLifecycleService: TestLifecycleService; let viewModel: AgentSessionsViewModel; + let instantiationService: TestInstantiationService; + + function createViewModel(): AgentSessionsViewModel { + return disposables.add(instantiationService.createInstance( + AgentSessionsViewModel, + { filterMenuId: MenuId.ViewTitle } + )); + } setup(() => { mockChatSessionsService = new MockChatSessionsService(); mockLifecycleService = disposables.add(new TestLifecycleService()); + instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); + instantiationService.stub(IChatSessionsService, mockChatSessionsService); + instantiationService.stub(ILifecycleService, mockLifecycleService); }); teardown(() => { @@ -37,10 +51,7 @@ suite('AgentSessionsViewModel', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('should initialize with empty sessions', () => { - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); + viewModel = createViewModel(); assert.strictEqual(viewModel.sessions.length, 0); }); @@ -66,10 +77,7 @@ suite('AgentSessionsViewModel', () => { }; mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); + viewModel = createViewModel(); await viewModel.resolve(undefined); @@ -110,10 +118,7 @@ suite('AgentSessionsViewModel', () => { mockChatSessionsService.registerChatSessionItemProvider(provider1); mockChatSessionsService.registerChatSessionItemProvider(provider2); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); + viewModel = createViewModel(); await viewModel.resolve(undefined); @@ -132,10 +137,7 @@ suite('AgentSessionsViewModel', () => { }; mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); + viewModel = createViewModel(); let willResolveFired = false; let didResolveFired = false; @@ -172,10 +174,7 @@ suite('AgentSessionsViewModel', () => { }; mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); + viewModel = createViewModel(); let sessionsChangedFired = false; disposables.add(viewModel.onDidChangeSessions(() => { @@ -211,10 +210,7 @@ suite('AgentSessionsViewModel', () => { }; mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); + viewModel = createViewModel(); await viewModel.resolve(undefined); @@ -248,10 +244,7 @@ suite('AgentSessionsViewModel', () => { }; mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); + viewModel = createViewModel(); await viewModel.resolve(undefined); @@ -290,10 +283,7 @@ suite('AgentSessionsViewModel', () => { mockChatSessionsService.registerChatSessionItemProvider(provider1); mockChatSessionsService.registerChatSessionItemProvider(provider2); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); + viewModel = createViewModel(); // First resolve all await viewModel.resolve(undefined); @@ -336,10 +326,7 @@ suite('AgentSessionsViewModel', () => { mockChatSessionsService.registerChatSessionItemProvider(provider1); mockChatSessionsService.registerChatSessionItemProvider(provider2); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); + viewModel = createViewModel(); await viewModel.resolve(['type-1', 'type-2']); @@ -362,10 +349,7 @@ suite('AgentSessionsViewModel', () => { }; mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); + viewModel = createViewModel(); const sessionsChangedPromise = Event.toPromise(viewModel.onDidChangeSessions); @@ -394,10 +378,7 @@ suite('AgentSessionsViewModel', () => { }; mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); + viewModel = createViewModel(); const sessionsChangedPromise = Event.toPromise(viewModel.onDidChangeSessions); @@ -426,10 +407,7 @@ suite('AgentSessionsViewModel', () => { }; mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); + viewModel = createViewModel(); const sessionsChangedPromise = Event.toPromise(viewModel.onDidChangeSessions); @@ -458,10 +436,7 @@ suite('AgentSessionsViewModel', () => { }; mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); + viewModel = createViewModel(); await viewModel.resolve(undefined); @@ -480,10 +455,7 @@ suite('AgentSessionsViewModel', () => { }; mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); + viewModel = createViewModel(); await viewModel.resolve(undefined); @@ -522,10 +494,7 @@ suite('AgentSessionsViewModel', () => { }; mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); + viewModel = createViewModel(); await viewModel.resolve(undefined); @@ -557,10 +526,7 @@ suite('AgentSessionsViewModel', () => { }; mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); + viewModel = createViewModel(); await viewModel.resolve(undefined); assert.strictEqual(viewModel.sessions.length, 1); @@ -587,10 +553,7 @@ suite('AgentSessionsViewModel', () => { }; mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); + viewModel = createViewModel(); await viewModel.resolve(undefined); @@ -616,10 +579,7 @@ suite('AgentSessionsViewModel', () => { }; mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); + viewModel = createViewModel(); await viewModel.resolve(undefined); @@ -648,10 +608,7 @@ suite('AgentSessionsViewModel', () => { }; mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); + viewModel = createViewModel(); // Make multiple rapid resolve calls const resolvePromises = [ @@ -706,10 +663,7 @@ suite('AgentSessionsViewModel', () => { mockChatSessionsService.registerChatSessionItemProvider(provider1); mockChatSessionsService.registerChatSessionItemProvider(provider2); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); + viewModel = createViewModel(); // First resolve all await viewModel.resolve(undefined); @@ -768,10 +722,7 @@ suite('AgentSessionsViewModel', () => { mockChatSessionsService.registerChatSessionItemProvider(provider1); mockChatSessionsService.registerChatSessionItemProvider(provider2); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); + viewModel = createViewModel(); // Call resolve with different types rapidly - they should accumulate const promise1 = viewModel.resolve('type-1'); @@ -866,8 +817,14 @@ suite('AgentSessionsViewModel - Helper Functions', () => { }; // Test with actual view model - const actualViewModel = new AgentSessionsViewModel(new MockChatSessionsService(), disposables.add(new TestLifecycleService())); - disposables.add(actualViewModel); + const instantiationService = workbenchInstantiationService(undefined, disposables); + const lifecycleService = disposables.add(new TestLifecycleService()); + instantiationService.stub(IChatSessionsService, new MockChatSessionsService()); + instantiationService.stub(ILifecycleService, lifecycleService); + const actualViewModel = disposables.add(instantiationService.createInstance( + AgentSessionsViewModel, + { filterMenuId: MenuId.ViewTitle } + )); assert.strictEqual(isAgentSessionsViewModel(actualViewModel), true); // Test with session object