agent sessions - add a view filter action to filter by provider type (#278021)

* agent sessions - add a view filter action to filter by provider type

* Update src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsViewFilter.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* feedback

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Benjamin Pasero
2025-11-18 10:00:18 +01:00
committed by GitHub
parent ae77536e70
commit 8f1ea102fe
8 changed files with 306 additions and 170 deletions

View File

@@ -223,6 +223,8 @@ export class MenuId {
static readonly TimelineTitle = new MenuId('TimelineTitle'); static readonly TimelineTitle = new MenuId('TimelineTitle');
static readonly TimelineTitleContext = new MenuId('TimelineTitleContext'); static readonly TimelineTitleContext = new MenuId('TimelineTitleContext');
static readonly TimelineFilterSubMenu = new MenuId('TimelineFilterSubMenu'); 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 AccountsContext = new MenuId('AccountsContext');
static readonly SidebarTitle = new MenuId('SidebarTitle'); static readonly SidebarTitle = new MenuId('SidebarTitle');
static readonly PanelTitle = new MenuId('PanelTitle'); static readonly PanelTitle = new MenuId('PanelTitle');

View File

@@ -12,7 +12,7 @@ import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/cont
import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
import { IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js'; import { IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js';
import { Codicon } from '../../../../../base/common/codicons.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 { localize, localize2 } from '../../../../../nls.js';
import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { basename, relativePath } from '../../../../../base/common/resources.js'; import { basename, relativePath } from '../../../../../base/common/resources.js';
@@ -89,13 +89,13 @@ export class ChatContinueInSessionActionItem extends ActionWidgetDropdownActionV
// Continue in Background // Continue in Background
const backgroundContrib = contributions.find(contrib => contrib.type === AgentSessionProviders.Background); const backgroundContrib = contributions.find(contrib => contrib.type === AgentSessionProviders.Background);
if (backgroundContrib && backgroundContrib.canDelegate !== false) { if (backgroundContrib && backgroundContrib.canDelegate !== false) {
actions.push(this.toAction(backgroundContrib, instantiationService)); actions.push(this.toAction(AgentSessionProviders.Background, backgroundContrib, instantiationService));
} }
// Continue in Cloud // Continue in Cloud
const cloudContrib = contributions.find(contrib => contrib.type === AgentSessionProviders.Cloud); const cloudContrib = contributions.find(contrib => contrib.type === AgentSessionProviders.Cloud);
if (cloudContrib && cloudContrib.canDelegate !== false) { 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 // 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 { return {
id: contrib.type, id: contrib.type,
enabled: true, enabled: true,
icon: contrib.type === AgentSessionProviders.Cloud ? Codicon.cloud : Codicon.collection, icon: getAgentSessionProviderIcon(provider),
class: undefined, class: undefined,
label: localize('continueSessionIn', "Continue in {0}", getAgentSessionProviderName(provider)),
tooltip: contrib.displayName, 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)) run: () => instantiationService.invokeFunction(accessor => new CreateRemoteAgentJobAction().run(accessor, contrib))
}; };
} }
private static toSetupAction(type: string, instantiationService: IInstantiationService): IActionWidgetDropdownAction { private static toSetupAction(provider: AgentSessionProviders, instantiationService: IInstantiationService): IActionWidgetDropdownAction {
const label = type === AgentSessionProviders.Cloud ?
localize('continueInCloud', "Continue in Cloud") :
localize('continueInBackground', "Continue in Background");
return { return {
id: type, id: provider,
enabled: true, enabled: true,
icon: type === AgentSessionProviders.Cloud ? Codicon.cloud : Codicon.collection, icon: getAgentSessionProviderIcon(provider),
class: undefined, class: undefined,
tooltip: label, label: localize('continueSessionIn', "Continue in {0}", getAgentSessionProviderName(provider)),
label, tooltip: localize('continueSessionIn', "Continue in {0}", getAgentSessionProviderName(provider)),
run: () => instantiationService.invokeFunction(accessor => { run: () => instantiationService.invokeFunction(accessor => {
const commandService = accessor.get(ICommandService); const commandService = accessor.get(ICommandService);
return commandService.executeCommand(CHAT_SETUP_ACTION_ID); return commandService.executeCommand(CHAT_SETUP_ACTION_ID);

View File

@@ -12,9 +12,12 @@ import { Disposable } from '../../../../../base/common/lifecycle.js';
import { ThemeIcon } from '../../../../../base/common/themables.js'; import { ThemeIcon } from '../../../../../base/common/themables.js';
import { URI } from '../../../../../base/common/uri.js'; import { URI } from '../../../../../base/common/uri.js';
import { localize } from '../../../../../nls.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 { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js';
import { ChatSessionStatus, IChatSessionItemProvider, IChatSessionsExtensionPoint, IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.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 //#region Interfaces, Types
@@ -74,9 +77,11 @@ export function isAgentSessionsViewModel(obj: IAgentSessionsViewModel | IAgentSe
//#endregion //#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<void>()); private readonly _onWillResolve = this._register(new Emitter<void>());
readonly onWillResolve = this._onWillResolve.event; readonly onWillResolve = this._onWillResolve.event;
@@ -87,15 +92,27 @@ export class AgentSessionsViewModel extends Disposable implements IAgentSessions
private readonly _onDidChangeSessions = this._register(new Emitter<void>()); private readonly _onDidChangeSessions = this._register(new Emitter<void>());
readonly onDidChangeSessions = this._onDidChangeSessions.event; 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<void>(100)); private readonly resolver = this._register(new ThrottledDelayer<void>(100));
private readonly providersToResolve = new Set<string | undefined>(); private readonly providersToResolve = new Set<string | undefined>();
private readonly filter: AgentSessionsViewFilter;
constructor( constructor(
options: IAgentSessionsViewModelOptions,
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService, @IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
@ILifecycleService private readonly lifecycleService: ILifecycleService, @ILifecycleService private readonly lifecycleService: ILifecycleService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) { ) {
super(); super();
this.filter = this._register(this.instantiationService.createInstance(AgentSessionsViewFilter, { filterMenuId: options.filterMenuId }));
this.registerListeners(); this.registerListeners();
this.resolve(undefined); 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.onDidChangeItemsProviders(({ chatSessionType: provider }) => this.resolve(provider)));
this._register(this.chatSessionsService.onDidChangeAvailability(() => this.resolve(undefined))); this._register(this.chatSessionsService.onDidChangeAvailability(() => this.resolve(undefined)));
this._register(this.chatSessionsService.onDidChangeSessionItems(provider => this.resolve(provider))); this._register(this.chatSessionsService.onDidChangeSessionItems(provider => this.resolve(provider)));
this._register(this.filter.onDidChange(() => this._onDidChangeSessions.fire()));
} }
async resolve(provider: string | string[] | undefined): Promise<void> { async resolve(provider: string | string[] | undefined): Promise<void> {
@@ -142,7 +160,7 @@ export class AgentSessionsViewModel extends Disposable implements IAgentSessions
const newSessions: IAgentSessionViewModel[] = []; const newSessions: IAgentSessionViewModel[] = [];
for (const provider of this.chatSessionsService.getAllChatSessionItemProviders()) { for (const provider of this.chatSessionsService.getAllChatSessionItemProviders()) {
if (!providersToResolve.includes(undefined) && !providersToResolve.includes(provider.chatSessionType)) { 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 continue; // skipped for resolving, preserve existing ones
} }
@@ -172,17 +190,17 @@ export class AgentSessionsViewModel extends Disposable implements IAgentSessions
let icon: ThemeIcon; let icon: ThemeIcon;
let providerLabel: string; let providerLabel: string;
switch ((provider.chatSessionType)) { switch ((provider.chatSessionType)) {
case localChatSessionType: case AgentSessionProviders.Local:
providerLabel = localize('chat.session.providerLabel.local', "Local"); providerLabel = getAgentSessionProviderName(AgentSessionProviders.Local);
icon = Codicon.vm; icon = getAgentSessionProviderIcon(AgentSessionProviders.Local);
break; break;
case AgentSessionProviders.Background: case AgentSessionProviders.Background:
providerLabel = localize('chat.session.providerLabel.background', "Background"); providerLabel = getAgentSessionProviderName(AgentSessionProviders.Background);
icon = Codicon.collection; icon = getAgentSessionProviderIcon(AgentSessionProviders.Background);
break; break;
case AgentSessionProviders.Cloud: case AgentSessionProviders.Cloud:
providerLabel = localize('chat.session.providerLabel.cloud', "Cloud"); providerLabel = getAgentSessionProviderName(AgentSessionProviders.Cloud);
icon = Codicon.cloud; icon = getAgentSessionProviderIcon(AgentSessionProviders.Cloud);
break; break;
default: { default: {
providerLabel = mapSessionContributionToType.get(provider.chatSessionType)?.name ?? provider.chatSessionType; 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.length = 0;
this.sessions.push(...newSessions); this._sessions.push(...newSessions);
this._onDidChangeSessions.fire(); this._onDidChangeSessions.fire();
} }

View File

@@ -3,6 +3,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information. * 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'; import { localChatSessionType } from '../../common/chatSessionsService.js';
export const AGENT_SESSIONS_VIEW_CONTAINER_ID = 'workbench.viewContainer.agentSessions'; export const AGENT_SESSIONS_VIEW_CONTAINER_ID = 'workbench.viewContainer.agentSessions';
@@ -13,3 +16,26 @@ export enum AgentSessionProviders {
Background = 'copilotcli', Background = 'copilotcli',
Cloud = 'copilot-cloud-agent', 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;
}
}

View File

@@ -4,13 +4,19 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import './media/agentsessionsactions.css'; import './media/agentsessionsactions.css';
import { localize } from '../../../../../nls.js'; import { localize, localize2 } from '../../../../../nls.js';
import { IAgentSessionViewModel } from './agentSessionViewModel.js'; import { IAgentSessionViewModel } from './agentSessionViewModel.js';
import { Action, IAction } from '../../../../../base/common/actions.js'; import { Action, IAction } from '../../../../../base/common/actions.js';
import { ActionViewItem, IActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { ActionViewItem, IActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js';
import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js';
import { EventHelper, h, hide, show } from '../../../../../base/browser/dom.js'; import { EventHelper, h, hide, show } from '../../../../../base/browser/dom.js';
import { assertReturnsDefined } from '../../../../../base/common/types.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 //#region Diff Statistics Action
@@ -102,3 +108,53 @@ export class AgentSessionDiffActionViewItem extends ActionViewItem {
} }
//#endregion //#endregion
//#region View Actions
registerAction2(class extends ViewAction<AgentSessionsView> {
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<AgentSessionsView> {
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

View File

@@ -10,7 +10,7 @@ import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/cont
import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js';
import { Registry } from '../../../../../platform/registry/common/platform.js'; import { Registry } from '../../../../../platform/registry/common/platform.js';
import { registerIcon } from '../../../../../platform/theme/common/iconRegistry.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 { ViewPaneContainer } from '../../../../browser/parts/views/viewPaneContainer.js';
import { IViewContainersRegistry, Extensions as ViewExtensions, ViewContainerLocation, IViewsRegistry, IViewDescriptor, IViewDescriptorService } from '../../../../common/views.js'; import { IViewContainersRegistry, Extensions as ViewExtensions, ViewContainerLocation, IViewsRegistry, IViewDescriptor, IViewDescriptorService } from '../../../../common/views.js';
import { ChatContextKeys } from '../../common/chatContextKeys.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 { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';
import { IHoverService } from '../../../../../platform/hover/browser/hover.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 { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js';
import { IThemeService } from '../../../../../platform/theme/common/themeService.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 { ButtonWithDropdown } from '../../../../../base/browser/ui/button/button.js';
import { IAction, Separator, toAction } from '../../../../../base/common/actions.js'; import { IAction, Separator, toAction } from '../../../../../base/common/actions.js';
import { FuzzyScore } from '../../../../../base/common/filters.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 { IChatSessionsService } from '../../common/chatSessionsService.js';
import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js';
import { getSessionItemContextOverlay, NEW_CHAT_SESSION_ACTION_ID } from '../chatSessions/common.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, @IMenuService private readonly menuService: IMenuService,
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
) { ) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); super({ ...options, titleMenuId: MenuId.AgentSessionsTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
this.registerActions();
} }
protected override renderBody(container: HTMLElement): void { protected override renderBody(container: HTMLElement): void {
@@ -94,9 +92,9 @@ export class AgentSessionsView extends ViewPane {
} }
private registerListeners(): void { private registerListeners(): void {
const list = assertReturnsDefined(this.list);
// Sessions List // Sessions List
const list = assertReturnsDefined(this.list);
this._register(this.onDidChangeBodyVisibility(visible => { this._register(this.onDidChangeBodyVisibility(visible => {
if (!visible || this.sessionsViewModel) { if (!visible || this.sessionsViewModel) {
return; return;
@@ -124,7 +122,7 @@ export class AgentSessionsView extends ViewPane {
})); }));
} }
private async openAgentSession(e: IOpenEvent<IAgentSessionViewModel | undefined>) { private async openAgentSession(e: IOpenEvent<IAgentSessionViewModel | undefined>): Promise<void> {
const session = e.element; const session = e.element;
if (!session) { if (!session) {
return; return;
@@ -172,48 +170,7 @@ export class AgentSessionsView extends ViewPane {
menu.dispose(); menu.dispose();
} }
private registerActions(): void { //#endregion
this._register(registerAction2(class extends ViewAction<AgentSessionsView> {
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<AgentSessionsView> {
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();
}
}));
}
//#region New Session Controls //#region New Session Controls
@@ -343,7 +300,7 @@ export class AgentSessionsView extends ViewPane {
} }
private createViewModel(): void { 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.list?.setInput(sessionsViewModel);
this._register(sessionsViewModel.onDidChangeSessions(() => { this._register(sessionsViewModel.onDidChangeSessions(() => {
@@ -370,6 +327,18 @@ export class AgentSessionsView extends ViewPane {
//#endregion //#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 { protected override layoutBody(height: number, width: number): void {
super.layoutBody(height, width); super.layoutBody(height, width);

View File

@@ -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<void>());
readonly onDidChange = this._onDidChange.event;
private _excludes = new Set<string>();
get excludes(): Set<string> { return this._excludes; }
private excludesContext: IContextKey<string>;
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<string>(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);
}
}));
}
}
}

View File

@@ -11,12 +11,15 @@ import { ThemeIcon } from '../../../../../base/common/themables.js';
import { URI } from '../../../../../base/common/uri.js'; import { URI } from '../../../../../base/common/uri.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
import { AgentSessionsViewModel, IAgentSessionViewModel, isAgentSession, isAgentSessionsViewModel, isLocalAgentSessionItem } from '../../browser/agentSessions/agentSessionViewModel.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 { LocalChatSessionUri } from '../../common/chatUri.js';
import { MockChatSessionsService } from '../common/mockChatSessionsService.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 { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';
import { Codicon } from '../../../../../base/common/codicons.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', () => { suite('AgentSessionsViewModel', () => {
@@ -24,10 +27,21 @@ suite('AgentSessionsViewModel', () => {
let mockChatSessionsService: MockChatSessionsService; let mockChatSessionsService: MockChatSessionsService;
let mockLifecycleService: TestLifecycleService; let mockLifecycleService: TestLifecycleService;
let viewModel: AgentSessionsViewModel; let viewModel: AgentSessionsViewModel;
let instantiationService: TestInstantiationService;
function createViewModel(): AgentSessionsViewModel {
return disposables.add(instantiationService.createInstance(
AgentSessionsViewModel,
{ filterMenuId: MenuId.ViewTitle }
));
}
setup(() => { setup(() => {
mockChatSessionsService = new MockChatSessionsService(); mockChatSessionsService = new MockChatSessionsService();
mockLifecycleService = disposables.add(new TestLifecycleService()); mockLifecycleService = disposables.add(new TestLifecycleService());
instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables));
instantiationService.stub(IChatSessionsService, mockChatSessionsService);
instantiationService.stub(ILifecycleService, mockLifecycleService);
}); });
teardown(() => { teardown(() => {
@@ -37,10 +51,7 @@ suite('AgentSessionsViewModel', () => {
ensureNoDisposablesAreLeakedInTestSuite(); ensureNoDisposablesAreLeakedInTestSuite();
test('should initialize with empty sessions', () => { test('should initialize with empty sessions', () => {
viewModel = disposables.add(new AgentSessionsViewModel( viewModel = createViewModel();
mockChatSessionsService,
mockLifecycleService
));
assert.strictEqual(viewModel.sessions.length, 0); assert.strictEqual(viewModel.sessions.length, 0);
}); });
@@ -66,10 +77,7 @@ suite('AgentSessionsViewModel', () => {
}; };
mockChatSessionsService.registerChatSessionItemProvider(provider); mockChatSessionsService.registerChatSessionItemProvider(provider);
viewModel = disposables.add(new AgentSessionsViewModel( viewModel = createViewModel();
mockChatSessionsService,
mockLifecycleService
));
await viewModel.resolve(undefined); await viewModel.resolve(undefined);
@@ -110,10 +118,7 @@ suite('AgentSessionsViewModel', () => {
mockChatSessionsService.registerChatSessionItemProvider(provider1); mockChatSessionsService.registerChatSessionItemProvider(provider1);
mockChatSessionsService.registerChatSessionItemProvider(provider2); mockChatSessionsService.registerChatSessionItemProvider(provider2);
viewModel = disposables.add(new AgentSessionsViewModel( viewModel = createViewModel();
mockChatSessionsService,
mockLifecycleService
));
await viewModel.resolve(undefined); await viewModel.resolve(undefined);
@@ -132,10 +137,7 @@ suite('AgentSessionsViewModel', () => {
}; };
mockChatSessionsService.registerChatSessionItemProvider(provider); mockChatSessionsService.registerChatSessionItemProvider(provider);
viewModel = disposables.add(new AgentSessionsViewModel( viewModel = createViewModel();
mockChatSessionsService,
mockLifecycleService
));
let willResolveFired = false; let willResolveFired = false;
let didResolveFired = false; let didResolveFired = false;
@@ -172,10 +174,7 @@ suite('AgentSessionsViewModel', () => {
}; };
mockChatSessionsService.registerChatSessionItemProvider(provider); mockChatSessionsService.registerChatSessionItemProvider(provider);
viewModel = disposables.add(new AgentSessionsViewModel( viewModel = createViewModel();
mockChatSessionsService,
mockLifecycleService
));
let sessionsChangedFired = false; let sessionsChangedFired = false;
disposables.add(viewModel.onDidChangeSessions(() => { disposables.add(viewModel.onDidChangeSessions(() => {
@@ -211,10 +210,7 @@ suite('AgentSessionsViewModel', () => {
}; };
mockChatSessionsService.registerChatSessionItemProvider(provider); mockChatSessionsService.registerChatSessionItemProvider(provider);
viewModel = disposables.add(new AgentSessionsViewModel( viewModel = createViewModel();
mockChatSessionsService,
mockLifecycleService
));
await viewModel.resolve(undefined); await viewModel.resolve(undefined);
@@ -248,10 +244,7 @@ suite('AgentSessionsViewModel', () => {
}; };
mockChatSessionsService.registerChatSessionItemProvider(provider); mockChatSessionsService.registerChatSessionItemProvider(provider);
viewModel = disposables.add(new AgentSessionsViewModel( viewModel = createViewModel();
mockChatSessionsService,
mockLifecycleService
));
await viewModel.resolve(undefined); await viewModel.resolve(undefined);
@@ -290,10 +283,7 @@ suite('AgentSessionsViewModel', () => {
mockChatSessionsService.registerChatSessionItemProvider(provider1); mockChatSessionsService.registerChatSessionItemProvider(provider1);
mockChatSessionsService.registerChatSessionItemProvider(provider2); mockChatSessionsService.registerChatSessionItemProvider(provider2);
viewModel = disposables.add(new AgentSessionsViewModel( viewModel = createViewModel();
mockChatSessionsService,
mockLifecycleService
));
// First resolve all // First resolve all
await viewModel.resolve(undefined); await viewModel.resolve(undefined);
@@ -336,10 +326,7 @@ suite('AgentSessionsViewModel', () => {
mockChatSessionsService.registerChatSessionItemProvider(provider1); mockChatSessionsService.registerChatSessionItemProvider(provider1);
mockChatSessionsService.registerChatSessionItemProvider(provider2); mockChatSessionsService.registerChatSessionItemProvider(provider2);
viewModel = disposables.add(new AgentSessionsViewModel( viewModel = createViewModel();
mockChatSessionsService,
mockLifecycleService
));
await viewModel.resolve(['type-1', 'type-2']); await viewModel.resolve(['type-1', 'type-2']);
@@ -362,10 +349,7 @@ suite('AgentSessionsViewModel', () => {
}; };
mockChatSessionsService.registerChatSessionItemProvider(provider); mockChatSessionsService.registerChatSessionItemProvider(provider);
viewModel = disposables.add(new AgentSessionsViewModel( viewModel = createViewModel();
mockChatSessionsService,
mockLifecycleService
));
const sessionsChangedPromise = Event.toPromise(viewModel.onDidChangeSessions); const sessionsChangedPromise = Event.toPromise(viewModel.onDidChangeSessions);
@@ -394,10 +378,7 @@ suite('AgentSessionsViewModel', () => {
}; };
mockChatSessionsService.registerChatSessionItemProvider(provider); mockChatSessionsService.registerChatSessionItemProvider(provider);
viewModel = disposables.add(new AgentSessionsViewModel( viewModel = createViewModel();
mockChatSessionsService,
mockLifecycleService
));
const sessionsChangedPromise = Event.toPromise(viewModel.onDidChangeSessions); const sessionsChangedPromise = Event.toPromise(viewModel.onDidChangeSessions);
@@ -426,10 +407,7 @@ suite('AgentSessionsViewModel', () => {
}; };
mockChatSessionsService.registerChatSessionItemProvider(provider); mockChatSessionsService.registerChatSessionItemProvider(provider);
viewModel = disposables.add(new AgentSessionsViewModel( viewModel = createViewModel();
mockChatSessionsService,
mockLifecycleService
));
const sessionsChangedPromise = Event.toPromise(viewModel.onDidChangeSessions); const sessionsChangedPromise = Event.toPromise(viewModel.onDidChangeSessions);
@@ -458,10 +436,7 @@ suite('AgentSessionsViewModel', () => {
}; };
mockChatSessionsService.registerChatSessionItemProvider(provider); mockChatSessionsService.registerChatSessionItemProvider(provider);
viewModel = disposables.add(new AgentSessionsViewModel( viewModel = createViewModel();
mockChatSessionsService,
mockLifecycleService
));
await viewModel.resolve(undefined); await viewModel.resolve(undefined);
@@ -480,10 +455,7 @@ suite('AgentSessionsViewModel', () => {
}; };
mockChatSessionsService.registerChatSessionItemProvider(provider); mockChatSessionsService.registerChatSessionItemProvider(provider);
viewModel = disposables.add(new AgentSessionsViewModel( viewModel = createViewModel();
mockChatSessionsService,
mockLifecycleService
));
await viewModel.resolve(undefined); await viewModel.resolve(undefined);
@@ -522,10 +494,7 @@ suite('AgentSessionsViewModel', () => {
}; };
mockChatSessionsService.registerChatSessionItemProvider(provider); mockChatSessionsService.registerChatSessionItemProvider(provider);
viewModel = disposables.add(new AgentSessionsViewModel( viewModel = createViewModel();
mockChatSessionsService,
mockLifecycleService
));
await viewModel.resolve(undefined); await viewModel.resolve(undefined);
@@ -557,10 +526,7 @@ suite('AgentSessionsViewModel', () => {
}; };
mockChatSessionsService.registerChatSessionItemProvider(provider); mockChatSessionsService.registerChatSessionItemProvider(provider);
viewModel = disposables.add(new AgentSessionsViewModel( viewModel = createViewModel();
mockChatSessionsService,
mockLifecycleService
));
await viewModel.resolve(undefined); await viewModel.resolve(undefined);
assert.strictEqual(viewModel.sessions.length, 1); assert.strictEqual(viewModel.sessions.length, 1);
@@ -587,10 +553,7 @@ suite('AgentSessionsViewModel', () => {
}; };
mockChatSessionsService.registerChatSessionItemProvider(provider); mockChatSessionsService.registerChatSessionItemProvider(provider);
viewModel = disposables.add(new AgentSessionsViewModel( viewModel = createViewModel();
mockChatSessionsService,
mockLifecycleService
));
await viewModel.resolve(undefined); await viewModel.resolve(undefined);
@@ -616,10 +579,7 @@ suite('AgentSessionsViewModel', () => {
}; };
mockChatSessionsService.registerChatSessionItemProvider(provider); mockChatSessionsService.registerChatSessionItemProvider(provider);
viewModel = disposables.add(new AgentSessionsViewModel( viewModel = createViewModel();
mockChatSessionsService,
mockLifecycleService
));
await viewModel.resolve(undefined); await viewModel.resolve(undefined);
@@ -648,10 +608,7 @@ suite('AgentSessionsViewModel', () => {
}; };
mockChatSessionsService.registerChatSessionItemProvider(provider); mockChatSessionsService.registerChatSessionItemProvider(provider);
viewModel = disposables.add(new AgentSessionsViewModel( viewModel = createViewModel();
mockChatSessionsService,
mockLifecycleService
));
// Make multiple rapid resolve calls // Make multiple rapid resolve calls
const resolvePromises = [ const resolvePromises = [
@@ -706,10 +663,7 @@ suite('AgentSessionsViewModel', () => {
mockChatSessionsService.registerChatSessionItemProvider(provider1); mockChatSessionsService.registerChatSessionItemProvider(provider1);
mockChatSessionsService.registerChatSessionItemProvider(provider2); mockChatSessionsService.registerChatSessionItemProvider(provider2);
viewModel = disposables.add(new AgentSessionsViewModel( viewModel = createViewModel();
mockChatSessionsService,
mockLifecycleService
));
// First resolve all // First resolve all
await viewModel.resolve(undefined); await viewModel.resolve(undefined);
@@ -768,10 +722,7 @@ suite('AgentSessionsViewModel', () => {
mockChatSessionsService.registerChatSessionItemProvider(provider1); mockChatSessionsService.registerChatSessionItemProvider(provider1);
mockChatSessionsService.registerChatSessionItemProvider(provider2); mockChatSessionsService.registerChatSessionItemProvider(provider2);
viewModel = disposables.add(new AgentSessionsViewModel( viewModel = createViewModel();
mockChatSessionsService,
mockLifecycleService
));
// Call resolve with different types rapidly - they should accumulate // Call resolve with different types rapidly - they should accumulate
const promise1 = viewModel.resolve('type-1'); const promise1 = viewModel.resolve('type-1');
@@ -866,8 +817,14 @@ suite('AgentSessionsViewModel - Helper Functions', () => {
}; };
// Test with actual view model // Test with actual view model
const actualViewModel = new AgentSessionsViewModel(new MockChatSessionsService(), disposables.add(new TestLifecycleService())); const instantiationService = workbenchInstantiationService(undefined, disposables);
disposables.add(actualViewModel); 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); assert.strictEqual(isAgentSessionsViewModel(actualViewModel), true);
// Test with session object // Test with session object