Path: blob/main/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts
4780 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';6import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';7import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';8import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';9import { IOpenEvent, WorkbenchCompressibleAsyncDataTree } from '../../../../../platform/list/browser/listService.js';10import { $, append, EventHelper } from '../../../../../base/browser/dom.js';11import { AgentSessionSection, IAgentSession, IAgentSessionsModel, IMarshalledAgentSessionContext, isAgentSession, isAgentSessionSection } from './agentSessionsModel.js';12import { AgentSessionListItem, AgentSessionRenderer, AgentSessionsAccessibilityProvider, AgentSessionsCompressionDelegate, AgentSessionsDataSource, AgentSessionsDragAndDrop, AgentSessionsIdentityProvider, AgentSessionsKeyboardNavigationLabelProvider, AgentSessionsListDelegate, AgentSessionSectionRenderer, AgentSessionsSorter, IAgentSessionsFilter, IAgentSessionsSorterOptions } from './agentSessionsViewer.js';13import { FuzzyScore } from '../../../../../base/common/filters.js';14import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js';15import { IChatSessionsService } from '../../common/chatSessionsService.js';16import { ICommandService } from '../../../../../platform/commands/common/commands.js';17import { ACTION_ID_NEW_CHAT } from '../actions/chatActions.js';18import { Event } from '../../../../../base/common/event.js';19import { Disposable } from '../../../../../base/common/lifecycle.js';20import { ITreeContextMenuEvent } from '../../../../../base/browser/ui/tree/tree.js';21import { MarshalledId } from '../../../../../base/common/marshallingIds.js';22import { Separator } from '../../../../../base/common/actions.js';23import { RenderIndentGuides, TreeFindMode } from '../../../../../base/browser/ui/tree/abstractTree.js';24import { IAgentSessionsService } from './agentSessionsService.js';25import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';26import { IListStyles } from '../../../../../base/browser/ui/list/listWidget.js';27import { IStyleOverride } from '../../../../../platform/theme/browser/defaultStyles.js';28import { IAgentSessionsControl } from './agentSessions.js';29import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js';30import { URI } from '../../../../../base/common/uri.js';31import { openSession } from './agentSessionsOpener.js';32import { IEditorService } from '../../../../services/editor/common/editorService.js';33import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js';3435export interface IAgentSessionsControlOptions extends IAgentSessionsSorterOptions {36readonly overrideStyles: IStyleOverride<IListStyles>;37readonly filter: IAgentSessionsFilter;3839getHoverPosition(): HoverPosition;40trackActiveEditorSession(): boolean;41}4243type AgentSessionOpenedClassification = {44owner: 'bpasero';45providerType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider type of the opened agent session.' };46comment: 'Event fired when a agent session is opened from the agent sessions control.';47};4849type AgentSessionOpenedEvent = {50providerType: string;51};5253export class AgentSessionsControl extends Disposable implements IAgentSessionsControl {5455private sessionsContainer: HTMLElement | undefined;56private sessionsList: WorkbenchCompressibleAsyncDataTree<IAgentSessionsModel, AgentSessionListItem, FuzzyScore> | undefined;5758private visible: boolean = true;5960private focusedAgentSessionArchivedContextKey: IContextKey<boolean>;61private focusedAgentSessionReadContextKey: IContextKey<boolean>;62private focusedAgentSessionTypeContextKey: IContextKey<string>;6364constructor(65private readonly container: HTMLElement,66private readonly options: IAgentSessionsControlOptions,67@IContextMenuService private readonly contextMenuService: IContextMenuService,68@IContextKeyService private readonly contextKeyService: IContextKeyService,69@IInstantiationService private readonly instantiationService: IInstantiationService,70@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,71@ICommandService private readonly commandService: ICommandService,72@IMenuService private readonly menuService: IMenuService,73@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,74@ITelemetryService private readonly telemetryService: ITelemetryService,75@IEditorService private readonly editorService: IEditorService,76) {77super();7879this.focusedAgentSessionArchivedContextKey = ChatContextKeys.isArchivedAgentSession.bindTo(this.contextKeyService);80this.focusedAgentSessionReadContextKey = ChatContextKeys.isReadAgentSession.bindTo(this.contextKeyService);81this.focusedAgentSessionTypeContextKey = ChatContextKeys.agentSessionType.bindTo(this.contextKeyService);8283this.createList(this.container);8485this.registerListeners();86}8788private registerListeners(): void {89this._register(this.editorService.onDidActiveEditorChange(() => this.revealAndFocusActiveEditorSession()));90}9192private revealAndFocusActiveEditorSession(): void {93if (94!this.options.trackActiveEditorSession() ||95!this.visible96) {97return;98}99100const input = this.editorService.activeEditor;101const resource = (input instanceof ChatEditorInput) ? input.sessionResource : input?.resource;102if (!resource) {103return;104}105106const matchingSession = this.agentSessionsService.model.getSession(resource);107if (matchingSession && this.sessionsList?.hasNode(matchingSession)) {108if (this.sessionsList.getRelativeTop(matchingSession) === null) {109this.sessionsList.reveal(matchingSession, 0.5); // only reveal when not already visible110}111112this.sessionsList.setFocus([matchingSession]);113this.sessionsList.setSelection([matchingSession]);114}115}116117private createList(container: HTMLElement): void {118this.sessionsContainer = append(container, $('.agent-sessions-viewer'));119120const sorter = new AgentSessionsSorter(this.options);121const list = this.sessionsList = this._register(this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree,122'AgentSessionsView',123this.sessionsContainer,124new AgentSessionsListDelegate(),125new AgentSessionsCompressionDelegate(),126[127this.instantiationService.createInstance(AgentSessionRenderer, this.options),128this.instantiationService.createInstance(AgentSessionSectionRenderer),129],130new AgentSessionsDataSource(this.options.filter, sorter),131{132accessibilityProvider: new AgentSessionsAccessibilityProvider(),133dnd: this.instantiationService.createInstance(AgentSessionsDragAndDrop),134identityProvider: new AgentSessionsIdentityProvider(),135horizontalScrolling: false,136multipleSelectionSupport: false,137findWidgetEnabled: true,138defaultFindMode: TreeFindMode.Filter,139keyboardNavigationLabelProvider: new AgentSessionsKeyboardNavigationLabelProvider(),140overrideStyles: this.options.overrideStyles,141expandOnlyOnTwistieClick: (element: unknown) => !(isAgentSessionSection(element) && element.section === AgentSessionSection.Archived && this.options.filter.getExcludes().archived),142twistieAdditionalCssClass: () => 'force-no-twistie',143collapseByDefault: (element: unknown) => isAgentSessionSection(element) && element.section === AgentSessionSection.Archived && this.options.filter.getExcludes().archived,144renderIndentGuides: RenderIndentGuides.None,145}146)) as WorkbenchCompressibleAsyncDataTree<IAgentSessionsModel, AgentSessionListItem, FuzzyScore>;147148ChatContextKeys.agentSessionsViewerFocused.bindTo(list.contextKeyService);149150const model = this.agentSessionsService.model;151152this._register(this.options.filter.onDidChange(async () => {153if (this.visible) {154this.updateArchivedSectionCollapseState();155list.updateChildren();156}157}));158159this._register(model.onDidChangeSessions(() => {160if (this.visible) {161list.updateChildren();162}163}));164165list.setInput(model);166167this._register(list.onDidOpen(e => this.openAgentSession(e)));168this._register(list.onContextMenu(e => this.showContextMenu(e)));169170this._register(list.onMouseDblClick(({ element }) => {171if (element === null) {172this.commandService.executeCommand(ACTION_ID_NEW_CHAT);173}174}));175176this._register(Event.any(list.onDidChangeFocus, model.onDidChangeSessions)(() => {177const focused = list.getFocus().at(0);178if (focused && isAgentSession(focused)) {179this.focusedAgentSessionArchivedContextKey.set(focused.isArchived());180this.focusedAgentSessionReadContextKey.set(focused.isRead());181this.focusedAgentSessionTypeContextKey.set(focused.providerType);182} else {183this.focusedAgentSessionArchivedContextKey.reset();184this.focusedAgentSessionReadContextKey.reset();185this.focusedAgentSessionTypeContextKey.reset();186}187}));188}189190private async openAgentSession(e: IOpenEvent<AgentSessionListItem | undefined>): Promise<void> {191const element = e.element;192if (!element || isAgentSessionSection(element)) {193return; // Section headers are not openable194}195196this.telemetryService.publicLog2<AgentSessionOpenedEvent, AgentSessionOpenedClassification>('agentSessionOpened', {197providerType: element.providerType198});199200await this.instantiationService.invokeFunction(openSession, element, e);201}202203private async showContextMenu({ element, anchor, browserEvent }: ITreeContextMenuEvent<AgentSessionListItem>): Promise<void> {204if (!element || isAgentSessionSection(element)) {205return; // No context menu for section headers206}207208EventHelper.stop(browserEvent, true);209210await this.chatSessionsService.activateChatSessionItemProvider(element.providerType);211212const contextOverlay: Array<[string, boolean | string]> = [];213contextOverlay.push([ChatContextKeys.isArchivedAgentSession.key, element.isArchived()]);214contextOverlay.push([ChatContextKeys.isReadAgentSession.key, element.isRead()]);215contextOverlay.push([ChatContextKeys.agentSessionType.key, element.providerType]);216const menu = this.menuService.createMenu(MenuId.AgentSessionsContext, this.contextKeyService.createOverlay(contextOverlay));217218const marshalledSession: IMarshalledAgentSessionContext = { session: element, $mid: MarshalledId.AgentSessionContext };219this.contextMenuService.showContextMenu({220getActions: () => Separator.join(...menu.getActions({ arg: marshalledSession, shouldForwardArgs: true }).map(([, actions]) => actions)),221getAnchor: () => anchor,222getActionsContext: () => marshalledSession,223});224225menu.dispose();226}227228openFind(): void {229this.sessionsList?.openFind();230}231232private updateArchivedSectionCollapseState(): void {233if (!this.sessionsList) {234return;235}236237const model = this.agentSessionsService.model;238for (const child of this.sessionsList.getNode(model).children) {239if (!isAgentSessionSection(child.element) || child.element.section !== AgentSessionSection.Archived) {240continue;241}242243const shouldCollapseArchived = this.options.filter.getExcludes().archived;244if (shouldCollapseArchived && !child.collapsed) {245this.sessionsList.collapse(child.element);246} else if (!shouldCollapseArchived && child.collapsed) {247this.sessionsList.expand(child.element);248}249break;250}251}252253refresh(): Promise<void> {254return this.agentSessionsService.model.resolve(undefined);255}256257async update(): Promise<void> {258await this.sessionsList?.updateChildren();259}260261setVisible(visible: boolean): void {262if (this.visible === visible) {263return;264}265266this.visible = visible;267268if (this.visible) {269this.sessionsList?.updateChildren();270}271}272273layout(height: number, width: number): void {274this.sessionsList?.layout(height, width);275}276277focus(): void {278this.sessionsList?.domFocus();279}280281clearFocus(): void {282this.sessionsList?.setFocus([]);283this.sessionsList?.setSelection([]);284}285286scrollToTop(): void {287if (this.sessionsList) {288this.sessionsList.scrollTop = 0;289}290}291292getFocus(): IAgentSession[] {293const focused = this.sessionsList?.getFocus() ?? [];294295return focused.filter(e => isAgentSession(e));296}297298reveal(sessionResource: URI): void {299if (!this.sessionsList) {300return;301}302303const session = this.agentSessionsService.model.getSession(sessionResource);304if (!session || !this.sessionsList.hasNode(session)) {305return;306}307308if (this.sessionsList.getRelativeTop(session) === null) {309this.sessionsList.reveal(session, 0.5); // only reveal when not already visible310}311312this.sessionsList.setFocus([session]);313this.sessionsList.setSelection([session]);314}315}316317318