mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-19 17:58:39 +00:00
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:
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<void>());
|
||||
readonly onWillResolve = this._onWillResolve.event;
|
||||
@@ -87,15 +92,27 @@ export class AgentSessionsViewModel extends Disposable implements IAgentSessions
|
||||
private readonly _onDidChangeSessions = this._register(new Emitter<void>());
|
||||
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 providersToResolve = new Set<string | undefined>();
|
||||
|
||||
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<void> {
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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
|
||||
|
||||
@@ -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<IAgentSessionViewModel | undefined>) {
|
||||
private async openAgentSession(e: IOpenEvent<IAgentSessionViewModel | undefined>): Promise<void> {
|
||||
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<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();
|
||||
}
|
||||
}));
|
||||
}
|
||||
//#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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user