debt - removal of old agent sessions views and co (#282712)

This commit is contained in:
Benjamin Pasero
2025-12-11 15:02:11 +01:00
committed by GitHub
parent 8b98007d86
commit e6aeab6051
29 changed files with 99 additions and 2971 deletions

View File

@@ -273,8 +273,6 @@ export default tseslint.config(
'src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts',
'src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts',
'src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts',
'src/vs/workbench/contrib/chat/browser/chatSessions/common.ts',
'src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts',
'src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts',
'src/vs/workbench/contrib/chat/common/annotations.ts',
'src/vs/workbench/contrib/chat/common/chat.ts',

View File

@@ -284,7 +284,6 @@ export class MenuId {
static readonly DiffEditorHunkToolbar = new MenuId('DiffEditorHunkToolbar');
static readonly DiffEditorSelectionToolbar = new MenuId('DiffEditorSelectionToolbar');
static readonly AgentSessionsViewerFilterSubMenu = new MenuId('AgentSessionsViewerFilterSubMenu');
static readonly AgentSessionsInstallMenu = new MenuId('AgentSessionsInstallMenu');
static readonly AgentSessionsContext = new MenuId('AgentSessionsContext');
static readonly AgentSessionsCreateSubMenu = new MenuId('AgentSessionsCreateSubMenu');
static readonly AgentSessionsToolbar = new MenuId('AgentSessionsToolbar');
@@ -292,12 +291,6 @@ export class MenuId {
static readonly ChatViewSessionTitleNavigationToolbar = new MenuId('ChatViewSessionTitleNavigationToolbar');
static readonly ChatViewSessionTitleToolbar = new MenuId('ChatViewSessionTitleToolbar');
/**
* @deprecated TODO@bpasero remove
*/
static readonly AgentSessionsViewTitle = new MenuId('AgentSessionsViewTitle');
static readonly AgentSessionsFilterSubMenu = new MenuId('AgentSessionsFilterSubMenu');
/**
* Create or reuse a `MenuId` with the given identifier
*/

View File

@@ -22,8 +22,6 @@ import { CustomTreeView, TreeViewPane } from '../../browser/parts/views/treeView
import { ViewPaneContainer } from '../../browser/parts/views/viewPaneContainer.js';
import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../common/contributions.js';
import { ICustomViewDescriptor, IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, Extensions as ViewContainerExtensions, ViewContainerLocation } from '../../common/views.js';
import { ChatContextKeyExprs } from '../../contrib/chat/common/chatContextKeys.js';
import { LEGACY_AGENT_SESSIONS_VIEW_ID } from '../../contrib/chat/common/constants.js';
import { VIEWLET_ID as DEBUG } from '../../contrib/debug/common/debug.js';
import { VIEWLET_ID as EXPLORER } from '../../contrib/files/common/files.js';
import { VIEWLET_ID as REMOTE } from '../../contrib/remote/browser/remoteExplorer.js';
@@ -241,12 +239,6 @@ const viewsContribution: IJSONSchema = {
items: remoteViewDescriptor,
default: []
},
'agentSessions': { //TODO@bpasero retire this eventually
description: localize('views.agentSessions', "Contributes views to Agent Sessions container in the Activity bar. To contribute to this container, the 'chatSessionsProvider' API proposal must be enabled."),
type: 'array',
items: viewDescriptor,
default: []
}
},
additionalProperties: {
description: localize('views.contributed', "Contributes views to contributed views container"),
@@ -521,17 +513,12 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
accessibilityHelpContent = new MarkdownString(item.accessibilityHelpContent);
}
let when = ContextKeyExpr.deserialize(item.when);
if (key === 'agentSessions') {
when = ContextKeyExpr.and(when, ChatContextKeyExprs.agentViewWhen);
}
const viewDescriptor: ICustomViewDescriptor = {
type: type,
ctorDescriptor: type === ViewType.Tree ? new SyncDescriptor(TreeViewPane) : new SyncDescriptor(WebviewViewPane),
id: item.id,
name: { value: item.name, original: item.name },
when,
when: ContextKeyExpr.deserialize(item.when),
containerIcon: icon || viewContainer?.icon,
containerTitle: item.contextualTitle || (viewContainer && (typeof viewContainer.title === 'string' ? viewContainer.title : viewContainer.title.value)),
canToggleVisibility: true,
@@ -643,7 +630,6 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
case 'debug': return this.viewContainersRegistry.get(DEBUG);
case 'scm': return this.viewContainersRegistry.get(SCM);
case 'remote': return this.viewContainersRegistry.get(REMOTE);
case 'agentSessions': return this.viewContainersRegistry.get(LEGACY_AGENT_SESSIONS_VIEW_ID);
default: return this.viewContainersRegistry.get(`workbench.view.extension.${value}`);
}
}

View File

@@ -47,7 +47,7 @@ import { ActiveEditorContext, IsCompactTitleBarContext } from '../../../../commo
import { IWorkbenchContribution } from '../../../../common/contributions.js';
import { IViewDescriptorService, ViewContainerLocation } from '../../../../common/views.js';
import { ChatEntitlement, IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js';
import { GroupDirection, IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js';
import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js';
import { ACTIVE_GROUP, AUX_WINDOW_GROUP, IEditorService } from '../../../../services/editor/common/editorService.js';
import { IHostService } from '../../../../services/host/browser/host.js';
import { IWorkbenchLayoutService, Parts } from '../../../../services/layout/browser/layoutService.js';
@@ -66,7 +66,7 @@ import { IChatSessionItem, IChatSessionsService, localChatSessionType } from '..
import { ISCMHistoryItemChangeRangeVariableEntry, ISCMHistoryItemChangeVariableEntry } from '../../common/chatVariableEntries.js';
import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from '../../common/chatViewModel.js';
import { IChatWidgetHistoryService } from '../../common/chatWidgetHistoryService.js';
import { ChatAgentLocation, ChatConfiguration, ChatModeKind, LEGACY_AGENT_SESSIONS_VIEW_ID } from '../../common/constants.js';
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';
import { ILanguageModelChatSelector, ILanguageModelsService } from '../../common/languageModels.js';
import { CopilotUsageExtensionFeatureId } from '../../common/languageModelStats.js';
import { ILanguageModelToolsConfirmationService } from '../../common/languageModelToolsConfirmationService.js';
@@ -77,7 +77,6 @@ import { ChatEditorInput, showClearEditingSessionConfirmation } from '../chatEdi
import { ChatViewPane } from '../chatViewPane.js';
import { convertBufferToScreenshotVariable } from '../contrib/screenshot.js';
import { clearChatEditor } from './chatClear.js';
import { IMarshalledChatSessionContext } from './chatSessionActions.js';
export const CHAT_CATEGORY = localize2('chat.category', 'Chat');
@@ -922,7 +921,7 @@ export function registerChatActions() {
commandService.executeCommand(buttonItem.id, {
session: contextItem.session,
$mid: MarshalledId.ChatSessionContext
} satisfies IMarshalledChatSessionContext);
});
}
// dismiss quick picker
@@ -1098,97 +1097,6 @@ export function registerChatActions() {
}
});
registerAction2(class OpenChatEditorInNewWindowAction extends Action2 {
constructor() {
super({
id: `workbench.action.chat.newChatInNewWindow`,
title: localize2('chatSessions.openNewChatInNewWindow', 'Open New Chat in New Window'),
f1: false,
category: CHAT_CATEGORY,
precondition: ChatContextKeys.enabled,
menu: {
id: MenuId.ViewTitle,
group: 'submenu',
order: 1,
when: ContextKeyExpr.equals('view', `${LEGACY_AGENT_SESSIONS_VIEW_ID}.local`),
}
});
}
async run(accessor: ServicesAccessor) {
const widgetService = accessor.get(IChatWidgetService);
await widgetService.openSession(ChatEditorInput.getNewEditorUri(), AUX_WINDOW_GROUP, {
pinned: true,
auxiliary: { compact: true, bounds: { width: 800, height: 640 } }
});
}
});
registerAction2(class NewChatInSideBarAction extends Action2 {
constructor() {
super({
id: `workbench.action.chat.newChatInSideBar`,
title: localize2('chatSessions.newChatInSideBar', 'Open New Chat in Side Bar'),
f1: false,
category: CHAT_CATEGORY,
precondition: ChatContextKeys.enabled,
menu: {
id: MenuId.ViewTitle,
group: 'submenu',
order: 1,
when: ContextKeyExpr.equals('view', `${LEGACY_AGENT_SESSIONS_VIEW_ID}.local`),
}
});
}
async run(accessor: ServicesAccessor) {
const widgetService = accessor.get(IChatWidgetService);
// Open the chat view in the sidebar and get the widget
const chatWidget = await widgetService.revealWidget();
if (chatWidget) {
// Clear the current chat to start a new one
await chatWidget.clear();
chatWidget.attachmentModel.clear(true);
chatWidget.input.relatedFiles?.clear();
// Focus the input area
chatWidget.focusInput();
}
}
});
registerAction2(class OpenChatInNewEditorGroupAction extends Action2 {
constructor() {
super({
id: 'workbench.action.chat.openNewChatToTheSide',
title: localize2('chat.openNewChatToTheSide.label', "Open New Chat Editor to the Side"),
category: CHAT_CATEGORY,
precondition: ChatContextKeys.enabled,
f1: false,
menu: {
id: MenuId.ViewTitle,
group: 'submenu',
order: 1,
when: ContextKeyExpr.equals('view', `${LEGACY_AGENT_SESSIONS_VIEW_ID}.local`),
}
});
}
async run(accessor: ServicesAccessor, ...args: unknown[]) {
const widgetService = accessor.get(IChatWidgetService);
const editorGroupService = accessor.get(IEditorGroupsService);
// Create a new editor group to the right
const newGroup = editorGroupService.addGroup(editorGroupService.activeGroup, GroupDirection.RIGHT);
editorGroupService.activateGroup(newGroup);
// Open a new chat editor in the new group
await widgetService.openSession(ChatEditorInput.getNewEditorUri(), newGroup.id, { pinned: true });
}
});
registerAction2(class ClearChatInputHistoryAction extends Action2 {
constructor() {
super({

View File

@@ -72,16 +72,6 @@ export class ChatAgentRecommendation extends Disposable implements IWorkbenchCon
icon: Codicon.extensions,
precondition: ContextKeyExpr.equals(availabilityContextId, true),
menu: [
{
id: MenuId.AgentSessionsInstallMenu,
group: '0_install',
when: ContextKeyExpr.equals(availabilityContextId, true)
},
{
id: MenuId.AgentSessionsViewTitle,
group: 'navigation@98',
when: ContextKeyExpr.equals(availabilityContextId, true)
},
{
id: MenuId.ChatNewMenu,
group: '4_recommendations',

View File

@@ -40,7 +40,6 @@ import { IChatWidgetService } from '../chat.js';
import { ctxHasEditorModification } from '../chatEditing/chatEditingEditorContextKeys.js';
import { CHAT_SETUP_ACTION_ID } from './chatActions.js';
import { PromptFileVariableKind, toPromptFileVariableEntry } from '../../common/chatVariableEntries.js';
import { NEW_CHAT_SESSION_ACTION_ID } from '../chatSessions/common.js';
export const enum ActionLocation {
ChatWidget = 'chatWidget',
@@ -201,6 +200,8 @@ export class ChatContinueInSessionActionItem extends ActionWidgetDropdownActionV
}
}
const NEW_CHAT_SESSION_ACTION_ID = 'workbench.action.chat.openNewSessionEditor';
class CreateRemoteAgentJobAction {
constructor() { }

View File

@@ -1,318 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Codicon } from '../../../../../base/common/codicons.js';
import { KeyCode } from '../../../../../base/common/keyCodes.js';
import { MarshalledId } from '../../../../../base/common/marshallingIds.js';
import Severity from '../../../../../base/common/severity.js';
import * as nls from '../../../../../nls.js';
import { localize } from '../../../../../nls.js';
import { Action2, MenuId, MenuRegistry } from '../../../../../platform/actions/common/actions.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
import { ILogService } from '../../../../../platform/log/common/log.js';
import { IViewsService } from '../../../../services/views/common/viewsService.js';
import { ChatContextKeys } from '../../common/chatContextKeys.js';
import { IChatService } from '../../common/chatService.js';
import { IChatSessionItem, IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js';
import { ChatConfiguration, LEGACY_AGENT_SESSIONS_VIEW_ID } from '../../common/constants.js';
import { AGENT_SESSIONS_VIEW_CONTAINER_ID, AGENT_SESSIONS_VIEW_ID } from '../agentSessions/agentSessions.js';
import { ChatViewPaneTarget, IChatWidgetService } from '../chat.js';
import { ACTION_ID_OPEN_CHAT, CHAT_CATEGORY } from './chatActions.js';
export interface IMarshalledChatSessionContext {
readonly $mid: MarshalledId.ChatSessionContext;
readonly session: IChatSessionItem;
}
export function isMarshalledChatSessionContext(thing: unknown): thing is IMarshalledChatSessionContext {
if (typeof thing === 'object' && thing !== null) {
const candidate = thing as IMarshalledChatSessionContext;
return candidate.$mid === MarshalledId.ChatSessionContext && typeof candidate.session === 'object' && candidate.session !== null;
}
return false;
}
export class RenameChatSessionAction extends Action2 {
static readonly id = 'workbench.action.chat.renameSession';
constructor() {
super({
id: RenameChatSessionAction.id,
title: localize('renameSession', "Rename"),
f1: false,
category: CHAT_CATEGORY,
icon: Codicon.pencil,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyCode.F2,
when: ContextKeyExpr.equals('focusedView', 'workbench.view.chat.sessions.local')
}
});
}
async run(accessor: ServicesAccessor, context?: IMarshalledChatSessionContext): Promise<void> {
if (!context) {
return;
}
// Handle marshalled context from menu actions
const label = context.session.label;
const chatSessionsService = accessor.get(IChatSessionsService);
const logService = accessor.get(ILogService);
const chatService = accessor.get(IChatService);
try {
// Find the chat sessions view and trigger inline rename mode
// This is similar to how file renaming works in the explorer
await chatSessionsService.setEditableSession(context.session.resource, {
validationMessage: (value: string) => {
if (!value || value.trim().length === 0) {
return { content: localize('renameSession.emptyName', "Name cannot be empty"), severity: Severity.Error };
}
if (value.length > 100) {
return { content: localize('renameSession.nameTooLong', "Name is too long (maximum 100 characters)"), severity: Severity.Error };
}
return null;
},
placeholder: localize('renameSession.placeholder', "Enter new name for chat session"),
startingValue: label,
onFinish: async (value: string, success: boolean) => {
if (success && value && value.trim() !== label) {
try {
const newTitle = value.trim();
chatService.setChatSessionTitle(context.session.resource, newTitle);
// Notify the local sessions provider that items have changed
chatSessionsService.notifySessionItemsChanged(localChatSessionType);
} catch (error) {
logService.error(
localize('renameSession.error', "Failed to rename chat session: {0}",
(error instanceof Error ? error.message : String(error)))
);
}
}
await chatSessionsService.setEditableSession(context.session.resource, null);
}
});
} catch (error) {
logService.error('Failed to rename chat session', error instanceof Error ? error.message : String(error));
}
}
}
/**
* Action to delete a chat session from history
*/
export class DeleteChatSessionAction extends Action2 {
static readonly id = 'workbench.action.chat.deleteSession';
constructor() {
super({
id: DeleteChatSessionAction.id,
title: localize('deleteSession', "Delete"),
f1: false,
category: CHAT_CATEGORY,
icon: Codicon.x,
});
}
async run(accessor: ServicesAccessor, context?: IMarshalledChatSessionContext): Promise<void> {
if (!context) {
return;
}
// Handle marshalled context from menu actions
const chatService = accessor.get(IChatService);
const dialogService = accessor.get(IDialogService);
const logService = accessor.get(ILogService);
const chatSessionsService = accessor.get(IChatSessionsService);
try {
// Show confirmation dialog
const result = await dialogService.confirm({
message: localize('deleteSession.confirm', "Are you sure you want to delete this chat session?"),
detail: localize('deleteSession.detail', "This action cannot be undone."),
primaryButton: localize('deleteSession.delete', "Delete"),
type: 'warning'
});
if (result.confirmed) {
await chatService.removeHistoryEntry(context.session.resource);
// Notify the local sessions provider that items have changed
chatSessionsService.notifySessionItemsChanged(localChatSessionType);
}
} catch (error) {
logService.error('Failed to delete chat session', error instanceof Error ? error.message : String(error));
}
}
}
/**
* Action to open a chat session in the sidebar (chat widget)
*/
export class OpenChatSessionInSidebarAction extends Action2 {
static readonly id = 'workbench.action.chat.openSessionInSidebar';
constructor() {
super({
id: OpenChatSessionInSidebarAction.id,
title: localize('chat.openSessionInSidebar.label', "Move Chat into Side Bar"),
category: CHAT_CATEGORY,
f1: false,
});
}
async run(accessor: ServicesAccessor, context?: IMarshalledChatSessionContext): Promise<void> {
const chatWidgetService = accessor.get(IChatWidgetService);
if (!context) {
return;
}
// TODO: this feels strange. Should we prefer moving the editor to the sidebar instead? @osortega
await chatWidgetService.openSession(context.session.resource, ChatViewPaneTarget);
}
}
/**
* Action to toggle the description display mode for Chat Sessions
*/
export class ToggleChatSessionsDescriptionDisplayAction extends Action2 {
static readonly id = 'workbench.action.chatSessions.toggleDescriptionDisplay';
constructor() {
super({
id: ToggleChatSessionsDescriptionDisplayAction.id,
title: localize('chatSessions.toggleDescriptionDisplay.label', "Show Rich Descriptions"),
category: CHAT_CATEGORY,
f1: false,
toggled: ContextKeyExpr.equals(`config.${ChatConfiguration.ShowAgentSessionsViewDescription}`, true)
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const configurationService = accessor.get(IConfigurationService);
const currentValue = configurationService.getValue(ChatConfiguration.ShowAgentSessionsViewDescription);
await configurationService.updateValue(
ChatConfiguration.ShowAgentSessionsViewDescription,
!currentValue
);
}
}
/**
* Action to toggle between 'view' and 'single-view' modes for Agent Sessions
*/
export class ToggleAgentSessionsViewLocationAction extends Action2 {
static readonly id = 'workbench.action.chatSessions.toggleNewCombinedView';
constructor() {
super({
id: ToggleAgentSessionsViewLocationAction.id,
title: localize('chatSessions.toggleViewLocation.label', "Combined Sessions View"),
category: CHAT_CATEGORY,
f1: false,
toggled: ContextKeyExpr.equals(`config.${ChatConfiguration.AgentSessionsViewLocation}`, 'single-view'),
menu: [
{
id: MenuId.ViewContainerTitle,
when: ContextKeyExpr.equals('viewContainer', LEGACY_AGENT_SESSIONS_VIEW_ID),
group: '2_togglenew',
order: 1
},
{
id: MenuId.ViewContainerTitle,
when: ContextKeyExpr.equals('viewContainer', AGENT_SESSIONS_VIEW_CONTAINER_ID),
group: '2_togglenew',
order: 1
}
]
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const configurationService = accessor.get(IConfigurationService);
const viewsService = accessor.get(IViewsService);
const currentValue = configurationService.getValue<string>(ChatConfiguration.AgentSessionsViewLocation);
const newValue = currentValue === 'single-view' ? 'view' : 'single-view';
await configurationService.updateValue(ChatConfiguration.AgentSessionsViewLocation, newValue);
const viewId = newValue === 'single-view' ? AGENT_SESSIONS_VIEW_ID : `${LEGACY_AGENT_SESSIONS_VIEW_ID}.local`;
await viewsService.openView(viewId, true);
}
}
// Register the menu item - show for all local chat sessions (including history items)
MenuRegistry.appendMenuItem(MenuId.AgentSessionsContext, {
command: {
id: RenameChatSessionAction.id,
title: localize('renameSession', "Rename"),
icon: Codicon.pencil
},
group: 'inline',
order: 1,
when: ContextKeyExpr.and(
ChatContextKeys.agentSessionType.isEqualTo(localChatSessionType),
ChatContextKeys.isCombinedAgentSessionsViewer.negate()
)
});
// Register delete menu item - only show for non-active sessions (history items)
MenuRegistry.appendMenuItem(MenuId.AgentSessionsContext, {
command: {
id: DeleteChatSessionAction.id,
title: localize('deleteSession', "Delete"),
icon: Codicon.x
},
group: 'inline',
order: 2,
when: ContextKeyExpr.and(
ChatContextKeys.isArchivedAgentSession.isEqualTo(true),
ChatContextKeys.isActiveAgentSession.isEqualTo(false)
)
});
MenuRegistry.appendMenuItem(MenuId.AgentSessionsContext, {
command: {
id: OpenChatSessionInSidebarAction.id,
title: localize('openSessionInSidebar', "Open in Sidebar")
},
group: 'navigation',
order: 3,
when: ChatContextKeys.isCombinedAgentSessionsViewer.negate()
});
// Register the toggle command for the ViewTitle menu
MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, {
command: {
id: ToggleChatSessionsDescriptionDisplayAction.id,
title: localize('chatSessions.toggleDescriptionDisplay.label', "Show Rich Descriptions"),
toggled: ContextKeyExpr.equals(`config.${ChatConfiguration.ShowAgentSessionsViewDescription}`, true)
},
group: '1_config',
order: 1,
when: ContextKeyExpr.equals('viewContainer', LEGACY_AGENT_SESSIONS_VIEW_ID),
});
MenuRegistry.appendMenuItem(MenuId.ViewTitle, {
command: {
id: ACTION_ID_OPEN_CHAT,
title: nls.localize2('interactiveSession.open', "New Chat Editor"),
icon: Codicon.plus
},
group: 'navigation',
order: 1,
when: ContextKeyExpr.equals('view', `${LEGACY_AGENT_SESSIONS_VIEW_ID}.local`),
});

View File

@@ -6,60 +6,14 @@
import { Codicon } from '../../../../../base/common/codicons.js';
import { localize2 } from '../../../../../nls.js';
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js';
import { registerSingleton, InstantiationType } from '../../../../../platform/instantiation/common/extensions.js';
import { registerIcon } from '../../../../../platform/theme/common/iconRegistry.js';
import { ViewPaneContainer } from '../../../../browser/parts/views/viewPaneContainer.js';
import { IViewContainersRegistry, ViewContainerLocation, IViewDescriptor, IViewsRegistry, Extensions as ViewExtensions } from '../../../../common/views.js';
import { ChatContextKeys } from '../../common/chatContextKeys.js';
import { ChatConfiguration } from '../../common/constants.js';
import { AGENT_SESSIONS_VIEW_CONTAINER_ID, AGENT_SESSIONS_VIEW_ID, AgentSessionsViewerOrientation, AgentSessionsViewerPosition } from './agentSessions.js';
import { AgentSessionsViewerOrientation, AgentSessionsViewerPosition } from './agentSessions.js';
import { IAgentSessionsService, AgentSessionsService } from './agentSessionsService.js';
import { AgentSessionsView } from './agentSessionsView.js';
import { Registry } from '../../../../../platform/registry/common/platform.js';
import { LocalAgentsSessionsProvider } from './localAgentSessionsProvider.js';
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../common/contributions.js';
import { ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js';
import { ArchiveAgentSessionAction, UnarchiveAgentSessionAction, RefreshAgentSessionsViewAction, FindAgentSessionAction, OpenAgentSessionInEditorGroupAction, OpenAgentSessionInNewEditorGroupAction, OpenAgentSessionInNewWindowAction, ShowAgentSessionsSidebar, HideAgentSessionsSidebar, RefreshAgentSessionsViewerAction, FindAgentSessionInViewerAction, MarkAgentSessionUnreadAction, MarkAgentSessionReadAction, FocusAgentSessionsAction } from './agentSessionsActions.js';
//#region View Container and View Registration
const chatAgentsIcon = registerIcon('chat-sessions-icon', Codicon.commentDiscussionSparkle, 'Icon for Agent Sessions View');
const AGENT_SESSIONS_VIEW_TITLE = localize2('agentSessions.view.label', "Agents");
const agentSessionsViewContainer = Registry.as<IViewContainersRegistry>(ViewExtensions.ViewContainersRegistry).registerViewContainer({
id: AGENT_SESSIONS_VIEW_CONTAINER_ID,
title: AGENT_SESSIONS_VIEW_TITLE,
icon: chatAgentsIcon,
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [AGENT_SESSIONS_VIEW_CONTAINER_ID, { mergeViewWithContainerWhenSingleView: true }]),
storageId: AGENT_SESSIONS_VIEW_CONTAINER_ID,
hideIfEmpty: true,
order: 6,
}, ViewContainerLocation.AuxiliaryBar);
const agentSessionsViewDescriptor: IViewDescriptor = {
id: AGENT_SESSIONS_VIEW_ID,
containerIcon: chatAgentsIcon,
containerTitle: AGENT_SESSIONS_VIEW_TITLE.value,
singleViewPaneContainerTitle: AGENT_SESSIONS_VIEW_TITLE.value,
name: AGENT_SESSIONS_VIEW_TITLE,
canToggleVisibility: false,
canMoveView: true,
openCommandActionDescriptor: {
id: AGENT_SESSIONS_VIEW_ID,
title: AGENT_SESSIONS_VIEW_TITLE
},
ctorDescriptor: new SyncDescriptor(AgentSessionsView),
when: ContextKeyExpr.and(
ChatContextKeys.Setup.hidden.negate(),
ChatContextKeys.Setup.disabled.negate(),
ContextKeyExpr.equals(`config.${ChatConfiguration.AgentSessionsViewLocation}`, 'single-view'),
)
};
Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews([agentSessionsViewDescriptor], agentSessionsViewContainer);
//#endregion
import { ArchiveAgentSessionAction, UnarchiveAgentSessionAction, OpenAgentSessionInEditorGroupAction, OpenAgentSessionInNewEditorGroupAction, OpenAgentSessionInNewWindowAction, ShowAgentSessionsSidebar, HideAgentSessionsSidebar, RefreshAgentSessionsViewerAction, FindAgentSessionInViewerAction, MarkAgentSessionUnreadAction, MarkAgentSessionReadAction, FocusAgentSessionsAction } from './agentSessionsActions.js';
//#region Actions and Menus
@@ -71,21 +25,11 @@ registerAction2(MarkAgentSessionReadAction);
registerAction2(OpenAgentSessionInNewWindowAction);
registerAction2(OpenAgentSessionInEditorGroupAction);
registerAction2(OpenAgentSessionInNewEditorGroupAction);
registerAction2(RefreshAgentSessionsViewAction);
registerAction2(FindAgentSessionAction);
registerAction2(RefreshAgentSessionsViewerAction);
registerAction2(FindAgentSessionInViewerAction);
registerAction2(ShowAgentSessionsSidebar);
registerAction2(HideAgentSessionsSidebar);
MenuRegistry.appendMenuItem(MenuId.AgentSessionsViewTitle, {
submenu: MenuId.AgentSessionsFilterSubMenu,
title: localize2('filterAgentSessions', "Filter Agent Sessions"),
group: 'navigation',
order: 100,
icon: Codicon.filter
} satisfies ISubmenuItem);
// --- Agent Sessions Toolbar
MenuRegistry.appendMenuItem(MenuId.AgentSessionsToolbar, {

View File

@@ -6,16 +6,9 @@
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 { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { IViewsService } from '../../../../services/views/common/viewsService.js';
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
import { LEGACY_AGENT_SESSIONS_VIEW_ID } from '../../common/constants.js';
import { ChatViewId } from '../chat.js';
import { IChatSessionItem, localChatSessionType } from '../../common/chatSessionsService.js';
import { foreground, listActiveSelectionForeground, registerColor, transparent } from '../../../../../platform/theme/common/colorRegistry.js';
export const AGENT_SESSIONS_VIEW_CONTAINER_ID = 'workbench.viewContainer.agentSessions';
export const AGENT_SESSIONS_VIEW_ID = 'workbench.view.agentSessions';
import { MarshalledId } from '../../../../../base/common/marshallingIds.js';
export enum AgentSessionProviders {
Local = localChatSessionType,
@@ -45,20 +38,6 @@ export function getAgentSessionProviderIcon(provider: AgentSessionProviders): Th
}
}
export function openAgentSessionsView(accessor: ServicesAccessor): void {
const viewService = accessor.get(IViewsService);
const configurationService = accessor.get(IConfigurationService);
const viewLocation = configurationService.getValue('chat.agentSessionsViewLocation');
if (viewLocation === 'single-view') {
viewService.openView(AGENT_SESSIONS_VIEW_ID, true);
} else if (viewLocation === 'view') {
viewService.openViewContainer(LEGACY_AGENT_SESSIONS_VIEW_ID, true);
} else {
viewService.openView(ChatViewId, true);
}
}
export enum AgentSessionsViewerOrientation {
Stacked = 1,
SideBySide,
@@ -91,3 +70,17 @@ export const agentSessionSelectedUnfocusedBadgeBorder = registerColor(
{ dark: transparent(foreground, 0.3), light: transparent(foreground, 0.3), hcDark: foreground, hcLight: foreground },
localize('agentSessionSelectedUnfocusedBadgeBorder', "Border color for the badges in selected agent session items when the view is unfocused.")
);
export interface IMarshalledChatSessionContext {
readonly $mid: MarshalledId.ChatSessionContext;
readonly session: IChatSessionItem;
}
export function isMarshalledChatSessionContext(thing: unknown): thing is IMarshalledChatSessionContext {
if (typeof thing === 'object' && thing !== null) {
const candidate = thing as IMarshalledChatSessionContext;
return candidate.$mid === MarshalledId.ChatSessionContext && typeof candidate.session === 'object' && candidate.session !== null;
}
return false;
}

View File

@@ -8,12 +8,9 @@ import { IAgentSession } from './agentSessionsModel.js';
import { Action2, MenuId } 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, AgentSessionsViewerOrientation, IAgentSessionsControl } from './agentSessions.js';
import { AgentSessionsViewerOrientation, IAgentSessionsControl, IMarshalledChatSessionContext, isMarshalledChatSessionContext } from './agentSessions.js';
import { IChatService } from '../../common/chatService.js';
import { AgentSessionsView } from './agentSessionsView.js';
import { ChatContextKeys } from '../../common/chatContextKeys.js';
import { IMarshalledChatSessionContext, isMarshalledChatSessionContext } from '../actions/chatSessionActions.js';
import { IChatEditorOptions } from '../chatEditor.js';
import { ChatViewId, IChatWidgetService } from '../chat.js';
import { ACTIVE_GROUP, AUX_WINDOW_GROUP, PreferredGroup, SIDE_GROUP } from '../../../../services/editor/common/editorService.js';
@@ -307,52 +304,6 @@ export class MarkAgentSessionReadAction extends BaseAgentSessionAction {
//#endregion
//#region View Actions
export class RefreshAgentSessionsViewAction extends ViewAction<AgentSessionsView> {
constructor() {
super({
id: 'agentSessionsView.refresh',
title: localize2('refresh', "Refresh Agent Sessions"),
icon: Codicon.refresh,
menu: {
id: MenuId.AgentSessionsViewTitle,
group: 'navigation',
order: 1
},
viewId: AGENT_SESSIONS_VIEW_ID
});
}
runInView(accessor: ServicesAccessor, view: AgentSessionsView): void {
view.refresh();
}
}
export class FindAgentSessionAction extends ViewAction<AgentSessionsView> {
constructor() {
super({
id: 'agentSessionsView.find',
title: localize2('find', "Find Agent Session"),
icon: Codicon.search,
menu: {
id: MenuId.AgentSessionsViewTitle,
group: 'navigation',
order: 2
},
viewId: AGENT_SESSIONS_VIEW_ID
});
}
runInView(accessor: ServicesAccessor, view: AgentSessionsView): void {
view.openFind();
}
}
//#endregion
//#region Sessions Control Toolbar
export class RefreshAgentSessionsViewerAction extends Action2 {

View File

@@ -15,44 +15,38 @@ import { FuzzyScore } from '../../../../../base/common/filters.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 } from '../chatSessions/common.js';
import { ACTION_ID_NEW_CHAT } from '../actions/chatActions.js';
import { IChatEditorOptions } from '../chatEditor.js';
import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js';
import { Event } from '../../../../../base/common/event.js';
import { Disposable } from '../../../../../base/common/lifecycle.js';
import { ITreeContextMenuEvent } from '../../../../../base/browser/ui/tree/tree.js';
import { MarshalledId } from '../../../../../base/common/marshallingIds.js';
import { Separator } from '../../../../../base/common/actions.js';
import { IChatService } from '../../common/chatService.js';
import { ChatViewPaneTarget, IChatWidgetService } from '../chat.js';
import { TreeFindMode } from '../../../../../base/browser/ui/tree/abstractTree.js';
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../../services/editor/common/editorService.js';
import { IMarshalledChatSessionContext } from '../actions/chatSessionActions.js';
import { ACTIVE_GROUP, SIDE_GROUP } from '../../../../services/editor/common/editorService.js';
import { IAgentSessionsService } from './agentSessionsService.js';
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
import { IListStyles } from '../../../../../base/browser/ui/list/listWidget.js';
import { IStyleOverride } from '../../../../../platform/theme/browser/defaultStyles.js';
import { ChatEditorInput } from '../chatEditorInput.js';
import { IAgentSessionsControl } from './agentSessions.js';
import { IAgentSessionsControl, IMarshalledChatSessionContext } from './agentSessions.js';
import { Schemas } from '../../../../../base/common/network.js';
import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js';
export interface IAgentSessionsControlOptions {
readonly overrideStyles?: IStyleOverride<IListStyles>;
readonly filter?: IAgentSessionsFilter;
readonly allowOpenSessionsInPanel?: boolean; // TODO@bpasero retire this option eventually
readonly trackActiveEditor?: boolean;
getHoverPosition(): HoverPosition;
}
type AgentSessionOpenedClassification = {
owner: 'bpasero';
source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'From where the session was opened.' };
providerType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The provider type of the opened agent session.' };
comment: 'Event fired when a agent session is opened from the agent sessions control.';
};
type AgentSessionOpenedEvent = {
source: 'agentsView' | 'chatView';
providerType: string;
};
@@ -65,57 +59,20 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo
constructor(
private readonly container: HTMLElement,
private readonly options: IAgentSessionsControlOptions | undefined,
private readonly options: IAgentSessionsControlOptions,
@IContextMenuService private readonly contextMenuService: IContextMenuService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
@ICommandService private readonly commandService: ICommandService,
@IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService,
@IChatService private readonly chatService: IChatService,
@IMenuService private readonly menuService: IMenuService,
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IEditorService private readonly editorService: IEditorService,
) {
super();
this.createList(this.container);
this.registerListeners();
}
private registerListeners(): void {
if (this.options?.trackActiveEditor) {
this._register(this.editorService.onDidActiveEditorChange(() => this.revealAndFocusActiveEditorSession()));
}
}
private revealAndFocusActiveEditorSession(): void {
if (!this.visible) {
return;
}
const input = this.editorService.activeEditor;
if (!(input instanceof ChatEditorInput)) {
return;
}
const sessionResource = input.sessionResource;
if (!sessionResource) {
return;
}
const matchingSession = this.agentSessionsService.model.getSession(sessionResource);
if (matchingSession && this.sessionsList?.hasNode(matchingSession)) {
if (this.sessionsList.getRelativeTop(matchingSession) === null) {
this.sessionsList.reveal(matchingSession, 0.5); // only reveal when not already visible
}
this.sessionsList.setFocus([matchingSession]);
this.sessionsList.setSelection([matchingSession]);
}
}
private createList(container: HTMLElement): void {
@@ -128,7 +85,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo
new AgentSessionsListDelegate(),
new AgentSessionsCompressionDelegate(),
[
this.instantiationService.createInstance(AgentSessionRenderer)
this.instantiationService.createInstance(AgentSessionRenderer, this.options),
],
new AgentSessionsDataSource(this.options?.filter, sorter),
{
@@ -176,7 +133,6 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo
}
this.telemetryService.publicLog2<AgentSessionOpenedEvent, AgentSessionOpenedClassification>('agentSessionOpened', {
source: this.options?.allowOpenSessionsInPanel ? 'chatView' : 'agentsView',
providerType: session.providerType
});
@@ -194,18 +150,16 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo
let options: IChatEditorOptions = {
...sessionOptions,
...e.editorOptions,
revealIfOpened: this.options?.allowOpenSessionsInPanel // always try to reveal if already opened
revealIfOpened: true // always try to reveal if already opened
};
await this.chatSessionsService.activateChatSessionItemProvider(session.providerType); // ensure provider is activated before trying to open
let target: typeof SIDE_GROUP | typeof ACTIVE_GROUP | typeof ChatViewPaneTarget | undefined;
if (e.sideBySide) {
target = this.options?.allowOpenSessionsInPanel ? ACTIVE_GROUP : SIDE_GROUP;
} else if (this.options?.allowOpenSessionsInPanel) {
target = ChatViewPaneTarget;
} else {
target = ACTIVE_GROUP;
} else {
target = ChatViewPaneTarget;
}
const isLocalChatSession = session.resource.scheme === Schemas.vscodeChatEditor || session.resource.scheme === Schemas.vscodeLocalChatSession;
@@ -224,11 +178,12 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo
EventHelper.stop(browserEvent, true);
const provider = await this.chatSessionsService.activateChatSessionItemProvider(session.providerType);
const contextOverlay = getSessionItemContextOverlay(session, provider, this.chatService, this.editorGroupsService);
contextOverlay.push([ChatContextKeys.isCombinedAgentSessionsViewer.key, true]);
await this.chatSessionsService.activateChatSessionItemProvider(session.providerType);
const contextOverlay: Array<[string, boolean | string]> = [];
contextOverlay.push([ChatContextKeys.isReadAgentSession.key, session.isRead()]);
contextOverlay.push([ChatContextKeys.isArchivedAgentSession.key, session.isArchived()]);
contextOverlay.push([ChatContextKeys.agentSessionType.key, session.providerType]);
const menu = this.menuService.createMenu(MenuId.AgentSessionsContext, this.contextKeyService.createOverlay(contextOverlay));
const marshalledSession: IMarshalledChatSessionContext = { session, $mid: MarshalledId.ChatSessionContext };

View File

@@ -1,246 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import './media/agentsessionsview.css';
import { localize } from '../../../../../nls.js';
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
import { IViewPaneOptions, ViewPane } from '../../../../browser/parts/views/viewPane.js';
import { IViewDescriptorService } from '../../../../common/views.js';
import { ChatContextKeys } from '../../common/chatContextKeys.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 } 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';
import { $, append } from '../../../../../base/browser/dom.js';
import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js';
import { ButtonWithDropdown } from '../../../../../base/browser/ui/button/button.js';
import { IAction, Separator, toAction } from '../../../../../base/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 { NEW_CHAT_SESSION_ACTION_ID } from '../chatSessions/common.js';
import { ACTION_ID_OPEN_CHAT } from '../actions/chatActions.js';
import { IProgressService } from '../../../../../platform/progress/common/progress.js';
import { DeferredPromise } from '../../../../../base/common/async.js';
import { Event } from '../../../../../base/common/event.js';
import { MutableDisposable } from '../../../../../base/common/lifecycle.js';
import { getActionBarActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js';
import { AgentSessionProviders } from './agentSessions.js';
import { AgentSessionsFilter } from './agentSessionsFilter.js';
import { AgentSessionsControl } from './agentSessionsControl.js';
import { IAgentSessionsService } from './agentSessionsService.js';
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
type AgentSessionsViewPaneOpenedClassification = {
owner: 'bpasero';
comment: 'Event fired when the agent sessions pane is opened';
};
export class AgentSessionsView extends ViewPane {
constructor(
options: IViewPaneOptions,
@IKeybindingService keybindingService: IKeybindingService,
@IContextMenuService contextMenuService: IContextMenuService,
@IConfigurationService configurationService: IConfigurationService,
@IContextKeyService contextKeyService: IContextKeyService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@IInstantiationService instantiationService: IInstantiationService,
@IOpenerService openerService: IOpenerService,
@IThemeService themeService: IThemeService,
@IHoverService hoverService: IHoverService,
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
@ICommandService private readonly commandService: ICommandService,
@IProgressService private readonly progressService: IProgressService,
@IMenuService private readonly menuService: IMenuService,
@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
) {
super({ ...options, titleMenuId: MenuId.AgentSessionsViewTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
this.registerListeners();
}
private registerListeners(): void {
const sessionsModel = this.agentSessionsService.model;
const didResolveDisposable = this._register(new MutableDisposable());
this._register(sessionsModel.onWillResolve(() => {
const didResolve = new DeferredPromise<void>();
didResolveDisposable.value = Event.once(sessionsModel.onDidResolve)(() => didResolve.complete());
this.progressService.withProgress(
{
location: this.id,
title: localize('agentSessions.refreshing', 'Refreshing agent sessions...'),
delay: 500
},
() => didResolve.p
);
}));
}
protected override renderBody(container: HTMLElement): void {
super.renderBody(container);
this.telemetryService.publicLog2<{}, AgentSessionsViewPaneOpenedClassification>('agentSessionsViewPaneOpened');
container.classList.add('agent-sessions-view');
// New Session
this.createNewSessionButton(container);
// Sessions Control
this.createSessionsControl(container);
}
//#region New Session Controls
private newSessionContainer: HTMLElement | undefined;
private createNewSessionButton(container: HTMLElement): void {
this.newSessionContainer = append(container, $('.agent-sessions-new-session-container'));
const newSessionButton = this._register(new ButtonWithDropdown(this.newSessionContainer, {
title: localize('agentSessions.newSession', "New Session"),
ariaLabel: localize('agentSessions.newSessionAriaLabel', "New Session"),
contextMenuProvider: this.contextMenuService,
actions: {
getActions: () => {
return this.getNewSessionActions();
}
},
addPrimaryActionToDropdown: false,
...defaultButtonStyles,
}));
newSessionButton.label = localize('agentSessions.newSession', "New Session");
this._register(newSessionButton.onDidClick(() => this.commandService.executeCommand(ACTION_ID_OPEN_CHAT)));
}
private getNewSessionActions(): IAction[] {
const actions: IAction[] = [];
// Default action
actions.push(toAction({
id: 'newChatSession.default',
label: localize('newChatSessionDefault', "New Local Session"),
run: () => this.commandService.executeCommand(ACTION_ID_OPEN_CHAT)
}));
// Background (CLI)
actions.push(toAction({
id: 'newChatSessionFromProvider.background',
label: localize('newBackgroundSession', "New Background Session"),
run: () => this.commandService.executeCommand(`${NEW_CHAT_SESSION_ACTION_ID}.${AgentSessionProviders.Background}`)
}));
// Cloud
actions.push(toAction({
id: 'newChatSessionFromProvider.cloud',
label: localize('newCloudSession', "New Cloud Session"),
run: () => this.commandService.executeCommand(`${NEW_CHAT_SESSION_ACTION_ID}.${AgentSessionProviders.Cloud}`)
}));
let addedSeparator = false;
for (const provider of this.chatSessionsService.getAllChatSessionContributions()) {
if (provider.type === AgentSessionProviders.Background || provider.type === AgentSessionProviders.Cloud) {
continue; // already added above
}
if (!addedSeparator) {
actions.push(new Separator());
addedSeparator = true;
}
const menuActions = this.menuService.getMenuActions(MenuId.AgentSessionsCreateSubMenu, this.scopedContextKeyService.createOverlay([
[ChatContextKeys.agentSessionType.key, provider.type]
]));
const primaryActions = getActionBarActions(menuActions, () => true).primary;
// Prefer provider creation actions...
if (primaryActions.length > 0) {
actions.push(...primaryActions);
}
// ...over our generic one
else {
actions.push(toAction({
id: `newChatSessionFromProvider.${provider.type}`,
label: localize('newChatSessionFromProvider', "New {0}", provider.displayName),
run: () => this.commandService.executeCommand(`${NEW_CHAT_SESSION_ACTION_ID}.${provider.type}`)
}));
}
}
// Install more
const installMenuActions = this.menuService.getMenuActions(MenuId.AgentSessionsInstallMenu, this.scopedContextKeyService, { shouldForwardArgs: true });
const installActionBar = getActionBarActions(installMenuActions, () => true);
if (installActionBar.primary.length > 0) {
actions.push(new Separator());
actions.push(...installActionBar.primary);
}
return actions;
}
//#endregion
//#region Sessions Control
private sessionsControl: AgentSessionsControl | undefined;
private createSessionsControl(container: HTMLElement): void {
const sessionsFilter = this._register(this.instantiationService.createInstance(AgentSessionsFilter, {
filterMenuId: MenuId.AgentSessionsFilterSubMenu,
}));
this.sessionsControl = this._register(this.instantiationService.createInstance(AgentSessionsControl,
container,
{
filter: sessionsFilter,
trackActiveEditor: true,
}
));
this.sessionsControl.setVisible(this.isBodyVisible());
this._register(this.onDidChangeBodyVisibility(visible => {
this.sessionsControl?.setVisible(visible);
}));
}
//#endregion
//#region Actions internal API
openFind(): void {
this.sessionsControl?.openFind();
}
refresh(): void {
this.sessionsControl?.refresh();
}
//#endregion
protected override layoutBody(height: number, width: number): void {
super.layoutBody(height, width);
let sessionsControlHeight = height;
sessionsControlHeight -= this.newSessionContainer?.offsetHeight ?? 0;
this.sessionsControl?.layout(sessionsControlHeight, width);
}
override focus(): void {
super.focus();
this.sessionsControl?.focus();
}
}

View File

@@ -30,10 +30,7 @@ import { fillEditorsDragData } from '../../../../browser/dnd.js';
import { ChatSessionStatus } from '../../common/chatSessionsService.js';
import { HoverStyle } from '../../../../../base/browser/ui/hover/hover.js';
import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js';
import { IWorkbenchLayoutService, Position } from '../../../../services/layout/browser/layoutService.js';
import { IViewDescriptorService, ViewContainerLocation } from '../../../../common/views.js';
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
import { AGENT_SESSIONS_VIEW_ID } from './agentSessions.js';
import { IntervalTimer } from '../../../../../base/common/async.js';
import { MenuWorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js';
import { MenuId } from '../../../../../platform/actions/common/actions.js';
@@ -68,6 +65,10 @@ interface IAgentSessionItemTemplate {
readonly disposables: IDisposable;
}
export interface IAgentSessionRendererOptions {
getHoverPosition(): HoverPosition;
}
export class AgentSessionRenderer implements ICompressibleTreeRenderer<IAgentSession, FuzzyScore, IAgentSessionItemTemplate> {
static readonly TEMPLATE_ID = 'agent-session';
@@ -75,10 +76,9 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer<IAgentSes
readonly templateId = AgentSessionRenderer.TEMPLATE_ID;
constructor(
private readonly options: IAgentSessionRendererOptions,
@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,
@IProductService private readonly productService: IProductService,
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
@IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService,
@IHoverService private readonly hoverService: IHoverService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@@ -299,18 +299,7 @@ export class AgentSessionRenderer implements ICompressibleTreeRenderer<IAgentSes
content: tooltip,
style: HoverStyle.Pointer,
position: {
hoverPosition: (() => {
const sideBarPosition = this.layoutService.getSideBarPosition();
const viewLocation = this.viewDescriptorService.getViewLocationById(AGENT_SESSIONS_VIEW_ID);
switch (viewLocation) {
case ViewContainerLocation.Sidebar:
return sideBarPosition === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT;
case ViewContainerLocation.AuxiliaryBar:
return sideBarPosition === Position.LEFT ? HoverPosition.LEFT : HoverPosition.RIGHT;
default:
return HoverPosition.RIGHT;
}
})()
hoverPosition: this.options.getHoverPosition()
}
}), { groupId: 'agent.sessions' })
);

View File

@@ -15,7 +15,10 @@ import { IChatModel } from '../../common/chatModel.js';
import { IChatDetail, IChatService, ResponseModelState } from '../../common/chatService.js';
import { ChatSessionStatus, IChatSessionItem, IChatSessionItemProvider, IChatSessionsService, localChatSessionType } from '../../common/chatSessionsService.js';
import { getChatSessionType } from '../../common/chatUri.js';
import { ChatSessionItemWithProvider } from '../chatSessions/common.js';
interface IChatSessionItemWithProvider extends IChatSessionItem {
readonly provider: IChatSessionItemProvider;
}
export class LocalAgentsSessionsProvider extends Disposable implements IChatSessionItemProvider, IWorkbenchContribution {
@@ -41,14 +44,12 @@ export class LocalAgentsSessionsProvider extends Disposable implements IChatSess
}
private registerListeners(): void {
this._register(this.chatSessionsService.registerChatModelChangeListeners(
this.chatService,
Schemas.vscodeLocalChatSession,
() => this._onDidChangeChatSessionItems.fire()
));
// Listen for global session items changes for our session type
this._register(this.chatSessionsService.onDidChangeSessionItems(sessionType => {
if (sessionType === this.chatSessionType) {
this._onDidChange.fire();
@@ -63,35 +64,8 @@ export class LocalAgentsSessionsProvider extends Disposable implements IChatSess
}));
}
private modelToStatus(model: IChatModel): ChatSessionStatus | undefined {
if (model.requestInProgress.get()) {
return ChatSessionStatus.InProgress;
}
const requests = model.getRequests();
if (requests.length > 0) {
// Check if the last request was completed successfully or failed
const lastRequest = requests[requests.length - 1];
if (lastRequest?.response) {
if (lastRequest.response.isCanceled || lastRequest.response.result?.errorDetails?.code === 'canceled') {
return ChatSessionStatus.Completed;
} else if (lastRequest.response.result?.errorDetails) {
return ChatSessionStatus.Failed;
} else if (lastRequest.response.isComplete) {
return ChatSessionStatus.Completed;
} else {
return ChatSessionStatus.InProgress;
}
}
}
return undefined;
}
async provideChatSessionItems(token: CancellationToken): Promise<IChatSessionItem[]> {
const sessions: ChatSessionItemWithProvider[] = [];
const sessions: IChatSessionItemWithProvider[] = [];
const sessionsByResource = new ResourceSet();
for (const sessionDetail of await this.chatService.getLiveSessionItems()) {
@@ -112,23 +86,17 @@ export class LocalAgentsSessionsProvider extends Disposable implements IChatSess
return sessions;
}
private async getHistoryItems(): Promise<ChatSessionItemWithProvider[]> {
private async getHistoryItems(): Promise<IChatSessionItemWithProvider[]> {
try {
const historyItems = await this.chatService.getHistorySessionItems();
return coalesce(historyItems.map(history => {
const sessionItem = this.toChatSessionItem(history);
return sessionItem ? {
...sessionItem,
//todo@bpasero remove this property once classic view is gone
history: true
} : undefined;
}));
return coalesce(historyItems.map(history => this.toChatSessionItem(history)));
} catch (error) {
return [];
}
}
private toChatSessionItem(chat: IChatDetail): ChatSessionItemWithProvider | undefined {
private toChatSessionItem(chat: IChatDetail): IChatSessionItemWithProvider | undefined {
const model = this.chatService.getSession(chat.sessionResource);
let description: string | undefined;
@@ -145,9 +113,7 @@ export class LocalAgentsSessionsProvider extends Disposable implements IChatSess
provider: this,
label: chat.title,
description,
status: model ?
this.modelToStatus(model) :
chatResponseStateToSessionStatus(chat.lastResponseState),
status: model ? this.modelToStatus(model) : this.chatResponseStateToStatus(chat.lastResponseState),
iconPath: Codicon.chatSparkle,
timing: chat.timing,
changes: chat.stats ? {
@@ -157,16 +123,37 @@ export class LocalAgentsSessionsProvider extends Disposable implements IChatSess
} : undefined
};
}
}
function chatResponseStateToSessionStatus(state: ResponseModelState): ChatSessionStatus {
switch (state) {
case ResponseModelState.Cancelled:
case ResponseModelState.Complete:
return ChatSessionStatus.Completed;
case ResponseModelState.Failed:
return ChatSessionStatus.Failed;
case ResponseModelState.Pending:
private modelToStatus(model: IChatModel): ChatSessionStatus | undefined {
if (model.requestInProgress.get()) {
return ChatSessionStatus.InProgress;
}
const lastRequest = model.getRequests().at(-1);
if (lastRequest?.response) {
if (lastRequest.response.isCanceled || lastRequest.response.result?.errorDetails?.code === 'canceled') {
return ChatSessionStatus.Completed;
} else if (lastRequest.response.result?.errorDetails) {
return ChatSessionStatus.Failed;
} else if (lastRequest.response.isComplete) {
return ChatSessionStatus.Completed;
} else {
return ChatSessionStatus.InProgress;
}
}
return undefined;
}
private chatResponseStateToStatus(state: ResponseModelState): ChatSessionStatus {
switch (state) {
case ResponseModelState.Cancelled:
case ResponseModelState.Complete:
return ChatSessionStatus.Completed;
case ResponseModelState.Failed:
return ChatSessionStatus.Failed;
case ResponseModelState.Pending:
return ChatSessionStatus.InProgress;
}
}
}

View File

@@ -1,19 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.agent-sessions-view {
display: flex;
flex-direction: column;
.agent-sessions-new-session-container {
padding: 6px 12px;
flex: 0 0 auto;
}
.agent-sessions-new-session-container .monaco-dropdown-button {
padding: 0 4px;
}
}

View File

@@ -81,7 +81,6 @@ import { registerNewChatActions } from './actions/chatNewActions.js';
import { registerChatPromptNavigationActions } from './actions/chatPromptNavigationActions.js';
import { registerQuickChatActions } from './actions/chatQuickInputActions.js';
import { ChatAgentRecommendation } from './actions/chatAgentRecommendationActions.js';
import { DeleteChatSessionAction, OpenChatSessionInSidebarAction, RenameChatSessionAction, ToggleAgentSessionsViewLocationAction, ToggleChatSessionsDescriptionDisplayAction } from './actions/chatSessionActions.js';
import { registerChatTitleActions } from './actions/chatTitleActions.js';
import { registerChatElicitationActions } from './actions/chatElicitationActions.js';
import { registerChatToolActions } from './actions/chatToolActions.js';
@@ -113,7 +112,6 @@ import { ChatPasteProvidersFeature } from './chatPasteProviders.js';
import { QuickChatService } from './chatQuick.js';
import { ChatResponseAccessibleView } from './chatResponseAccessibleView.js';
import { ChatTerminalOutputAccessibleView } from './chatTerminalOutputAccessibleView.js';
import { ChatSessionsView, ChatSessionsViewContrib } from './chatSessions/view/chatSessionsView.js';
import { ChatSetupContribution, ChatTeardownContribution } from './chatSetup/chatSetupContributions.js';
import { ChatStatusBarEntry } from './chatStatus/chatStatusEntry.js';
import { ChatVariablesService } from './chatVariables.js';
@@ -571,16 +569,6 @@ configurationRegistry.registerConfiguration({
mode: 'auto'
}
},
[ChatConfiguration.AgentSessionsViewLocation]: {
type: 'string',
enum: ['disabled', 'view', 'single-view'], // TODO@bpasero remove this setting eventually
description: nls.localize('chat.sessionsViewLocation.description', "Controls where to show the agent sessions menu."),
default: 'disabled',
tags: ['preview', 'experimental'],
experiment: {
mode: 'auto'
}
},
[mcpDiscoverySection]: {
type: 'object',
properties: Object.fromEntries(allDiscoverySources.map(k => [k, { type: 'boolean', description: discoverySourceSettingsLabel[k] }])),
@@ -819,11 +807,6 @@ configurationRegistry.registerConfiguration({
default: false,
scope: ConfigurationScope.WINDOW
},
[ChatConfiguration.ShowAgentSessionsViewDescription]: {
type: 'boolean',
description: nls.localize('chat.showAgentSessionsViewDescription', "Controls whether session descriptions are displayed on a second row in the Chat Sessions view."),
default: true,
},
'chat.allowAnonymousAccess': { // TODO@bpasero remove me eventually
type: 'boolean',
description: nls.localize('chat.allowAnonymousAccess', "Controls whether anonymous access is allowed in chat."),
@@ -1211,8 +1194,6 @@ registerWorkbenchContribution2(ChatTransferContribution.ID, ChatTransferContribu
registerWorkbenchContribution2(ChatContextContributions.ID, ChatContextContributions, WorkbenchPhase.AfterRestored);
registerWorkbenchContribution2(ChatResponseResourceFileSystemProvider.ID, ChatResponseResourceFileSystemProvider, WorkbenchPhase.AfterRestored);
registerWorkbenchContribution2(PromptUrlHandler.ID, PromptUrlHandler, WorkbenchPhase.BlockRestore);
registerWorkbenchContribution2(ChatSessionsViewContrib.ID, ChatSessionsViewContrib, WorkbenchPhase.AfterRestored);
registerWorkbenchContribution2(ChatSessionsView.ID, ChatSessionsView, WorkbenchPhase.BlockRestore);
registerWorkbenchContribution2(ChatEditingNotebookFileSystemProviderContrib.ID, ChatEditingNotebookFileSystemProviderContrib, WorkbenchPhase.BlockStartup);
registerWorkbenchContribution2(UserToolSetsContributions.ID, UserToolSetsContributions, WorkbenchPhase.Eventually);
registerWorkbenchContribution2(PromptLanguageFeaturesProvider.ID, PromptLanguageFeaturesProvider, WorkbenchPhase.Eventually);
@@ -1236,7 +1217,7 @@ registerChatEditorActions();
registerChatElicitationActions();
registerChatToolActions();
registerLanguageModelActions();
registerAction2(ConfigureToolSets);
registerEditorFeature(ChatPasteProvidersFeature);
@@ -1268,11 +1249,4 @@ registerSingleton(IChatTodoListService, ChatTodoListService, InstantiationType.D
registerSingleton(IChatOutputRendererService, ChatOutputRendererService, InstantiationType.Delayed);
registerSingleton(IChatLayoutService, ChatLayoutService, InstantiationType.Delayed);
registerAction2(ConfigureToolSets);
registerAction2(RenameChatSessionAction);
registerAction2(DeleteChatSessionAction);
registerAction2(OpenChatSessionInSidebarAction);
registerAction2(ToggleChatSessionsDescriptionDisplayAction);
registerAction2(ToggleAgentSessionsViewLocationAction);
ChatWidget.CONTRIBS.push(ChatDynamicVariableModel);

View File

@@ -25,7 +25,6 @@ import { ILabelService } from '../../../../platform/label/common/label.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { isDark } from '../../../../platform/theme/common/theme.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { IEditableData } from '../../../common/views.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js';
import { ExtensionsRegistry } from '../../../services/extensions/common/extensionsRegistry.js';
@@ -33,10 +32,9 @@ import { ChatEditorInput } from '../browser/chatEditorInput.js';
import { IChatAgentAttachmentCapabilities, IChatAgentData, IChatAgentService } from '../common/chatAgents.js';
import { ChatContextKeys } from '../common/chatContextKeys.js';
import { ChatSessionStatus, IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemProvider, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsExtensionPoint, IChatSessionsService, localChatSessionType, SessionOptionsChangedCallback } from '../common/chatSessionsService.js';
import { LEGACY_AGENT_SESSIONS_VIEW_ID, ChatAgentLocation, ChatModeKind } from '../common/constants.js';
import { ChatAgentLocation, ChatModeKind } from '../common/constants.js';
import { CHAT_CATEGORY } from './actions/chatActions.js';
import { IChatEditorOptions } from './chatEditor.js';
import { NEW_CHAT_SESSION_ACTION_ID } from './chatSessions/common.js';
import { IChatModel } from '../common/chatModel.js';
import { IChatService, IChatToolInvocation } from '../common/chatService.js';
import { autorun, autorunIterableDelta, observableSignalFromEvent } from '../../../../base/common/observable.js';
@@ -277,7 +275,6 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
private readonly _sessionTypeInputPlaceholders: Map<string, string> = new Map();
private readonly _sessions = new ResourceMap<ContributedChatSessionData>();
private readonly _editableSessions = new ResourceMap<IEditableData>();
private readonly _hasCanDelegateProvidersKey: IContextKey<boolean>;
@@ -489,56 +486,9 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
const rawMenuActions = this._menuService.getMenuActions(MenuId.AgentSessionsCreateSubMenu, contextKeyService);
const menuActions = rawMenuActions.map(value => value[1]).flat();
const whenClause = ContextKeyExpr.and(
ContextKeyExpr.equals('view', `${LEGACY_AGENT_SESSIONS_VIEW_ID}.${contribution.type}`)
);
const disposables = new DisposableStore();
// If there's exactly one action, inline it
if (menuActions.length === 1) {
const first = menuActions[0];
if (first instanceof MenuItemAction) {
disposables.add(MenuRegistry.appendMenuItem(MenuId.ViewTitle, {
group: 'navigation',
title: first.label,
icon: Codicon.plus,
order: 1,
when: whenClause,
command: first.item,
}));
}
}
if (menuActions.length) {
disposables.add(MenuRegistry.appendMenuItem(MenuId.ViewTitle, {
group: 'navigation',
title: localize('interactiveSession.chatSessionSubMenuTitle', "Create chat session"),
icon: Codicon.plus,
order: 1,
when: whenClause,
submenu: MenuId.AgentSessionsCreateSubMenu,
isSplitButton: menuActions.length > 1
}));
} else {
// We control creation instead
disposables.add(MenuRegistry.appendMenuItem(MenuId.ViewTitle, {
command: {
id: `${NEW_CHAT_SESSION_ACTION_ID}.${contribution.type}`,
title: localize('interactiveSession.openNewSessionEditor', "New {0}", contribution.displayName),
icon: Codicon.plus,
source: {
id: extensionDescription.identifier.value,
title: extensionDescription.displayName || extensionDescription.name,
}
},
group: 'navigation',
order: 1,
when: whenClause,
}));
}
// Also mirror all create submenu actions into the global Chat New menu
// Mirror all create submenu actions into the global Chat New menu
for (const action of menuActions) {
if (action instanceof MenuItemAction) {
disposables.add(MenuRegistry.appendMenuItem(MenuId.ChatNewMenu, {
@@ -1037,25 +987,6 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
return !!session?.setOption(optionId, value);
}
// Implementation of editable session methods
public async setEditableSession(sessionResource: URI, data: IEditableData | null): Promise<void> {
if (!data) {
this._editableSessions.delete(sessionResource);
} else {
this._editableSessions.set(sessionResource, data);
}
// Trigger refresh of the session views that might need to update their rendering
this._onDidChangeSessionItems.fire(localChatSessionType);
}
public getEditableData(sessionResource: URI): IEditableData | undefined {
return this._editableSessions.get(sessionResource);
}
public isEditable(sessionResource: URI): boolean {
return this._editableSessions.has(sessionResource);
}
public notifySessionItemsChanged(chatSessionType: string): void {
this._onDidChangeSessionItems.fire(chatSessionType);
}

View File

@@ -1,136 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { fromNow } from '../../../../../base/common/date.js';
import { Schemas } from '../../../../../base/common/network.js';
import { EditorInput } from '../../../../common/editor/editorInput.js';
import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js';
import { ChatContextKeys } from '../../common/chatContextKeys.js';
import { IChatService } from '../../common/chatService.js';
import { IChatSessionItem, IChatSessionItemProvider, localChatSessionType } from '../../common/chatSessionsService.js';
import { ChatEditorInput } from '../chatEditorInput.js';
export const NEW_CHAT_SESSION_ACTION_ID = 'workbench.action.chat.openNewSessionEditor';
export type ChatSessionItemWithProvider = IChatSessionItem & {
readonly provider: IChatSessionItemProvider;
relativeTime?: string;
relativeTimeFullWord?: string;
hideRelativeTime?: boolean;
};
export function isChatSession(schemes: readonly string[], editor?: EditorInput): editor is ChatEditorInput {
if (!(editor instanceof ChatEditorInput)) {
return false;
}
if (!schemes.includes(editor.resource?.scheme) && editor.resource?.scheme !== Schemas.vscodeLocalChatSession && editor.resource?.scheme !== Schemas.vscodeChatEditor) {
return false;
}
if (editor.options.ignoreInView) {
return false;
}
return true;
}
// Helper function to update relative time for chat sessions (similar to timeline)
function updateRelativeTime(item: ChatSessionItemWithProvider, lastRelativeTime: string | undefined): string | undefined {
if (item.timing?.startTime) {
item.relativeTime = fromNow(item.timing.startTime);
item.relativeTimeFullWord = fromNow(item.timing.startTime, false, true);
if (lastRelativeTime === undefined || item.relativeTime !== lastRelativeTime) {
lastRelativeTime = item.relativeTime;
item.hideRelativeTime = false;
} else {
item.hideRelativeTime = true;
}
} else {
// Clear timestamp properties if no timestamp
item.relativeTime = undefined;
item.relativeTimeFullWord = undefined;
item.hideRelativeTime = false;
}
return lastRelativeTime;
}
// Helper function to extract timestamp from session item
export function extractTimestamp(item: IChatSessionItem): number | undefined {
// Use timing.startTime if available from the API
if (item.timing?.startTime) {
return item.timing.startTime;
}
// For other items, timestamp might already be set
if ('timestamp' in item) {
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
return (item as any).timestamp;
}
return undefined;
}
// Helper function to sort sessions by timestamp (newest first)
function sortSessionsByTimestamp(sessions: ChatSessionItemWithProvider[]): void {
sessions.sort((a, b) => {
const aTime = a.timing?.startTime ?? 0;
const bTime = b.timing?.startTime ?? 0;
return bTime - aTime; // newest first
});
}
// Helper function to apply time grouping to a list of sessions
function applyTimeGrouping(sessions: ChatSessionItemWithProvider[]): void {
let lastRelativeTime: string | undefined;
sessions.forEach(session => {
lastRelativeTime = updateRelativeTime(session, lastRelativeTime);
});
}
// Helper function to process session items with timestamps, sorting, and grouping
export function processSessionsWithTimeGrouping(sessions: ChatSessionItemWithProvider[]): ChatSessionItemWithProvider[] {
const sessionsTemp = [...sessions];
// Only process if we have sessions with timestamps
if (sessions.some(session => session.timing?.startTime !== undefined)) {
sortSessionsByTimestamp(sessionsTemp);
applyTimeGrouping(sessionsTemp);
}
return sessionsTemp;
}
// Helper function to create context overlay for session items
export function getSessionItemContextOverlay(
session: IChatSessionItem,
provider?: IChatSessionItemProvider,
chatService?: IChatService,
editorGroupsService?: IEditorGroupsService
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): [string, any][] {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const overlay: [string, any][] = [];
if (provider) {
overlay.push([ChatContextKeys.agentSessionType.key, provider.chatSessionType]);
}
// Mark history items
overlay.push([ChatContextKeys.isArchivedAgentSession.key, session.archived]);
// Mark active sessions - check if session is currently open in editor or widget
let isActiveSession = false;
if (!session.archived && provider?.chatSessionType === localChatSessionType) {
// Local non-history sessions are always active
isActiveSession = true;
} else if (session.archived && chatService && editorGroupsService) {
isActiveSession = !!chatService.getSession(session.resource);
}
overlay.push([ChatContextKeys.isActiveAgentSession.key, isActiveSession]);
return overlay;
}

View File

@@ -1,277 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Codicon } from '../../../../../../base/common/codicons.js';
import { Disposable } from '../../../../../../base/common/lifecycle.js';
import * as nls from '../../../../../../nls.js';
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js';
import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js';
import { SyncDescriptor } from '../../../../../../platform/instantiation/common/descriptors.js';
import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js';
import { ILogService } from '../../../../../../platform/log/common/log.js';
import { IProductService } from '../../../../../../platform/product/common/productService.js';
import { Registry } from '../../../../../../platform/registry/common/platform.js';
import { IStorageService } from '../../../../../../platform/storage/common/storage.js';
import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js';
import { registerIcon } from '../../../../../../platform/theme/common/iconRegistry.js';
import { IThemeService } from '../../../../../../platform/theme/common/themeService.js';
import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js';
import { ViewPaneContainer } from '../../../../../browser/parts/views/viewPaneContainer.js';
import { IWorkbenchContribution } from '../../../../../common/contributions.js';
import { Extensions, IViewContainersRegistry, IViewDescriptor, IViewDescriptorService, IViewsRegistry, ViewContainerLocation } from '../../../../../common/views.js';
import { IExtensionService } from '../../../../../services/extensions/common/extensions.js';
import { IWorkbenchLayoutService } from '../../../../../services/layout/browser/layoutService.js';
import { ChatContextKeyExprs } from '../../../common/chatContextKeys.js';
import { IChatSessionItemProvider, IChatSessionsExtensionPoint, IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js';
import { LEGACY_AGENT_SESSIONS_VIEW_ID } from '../../../common/constants.js';
import { ACTION_ID_OPEN_CHAT } from '../../actions/chatActions.js';
import { SessionsViewPane } from './sessionsViewPane.js';
export class ChatSessionsView extends Disposable implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.chatSessionsView';
constructor() {
super();
this.registerViewContainer();
}
private registerViewContainer(): void {
Registry.as<IViewContainersRegistry>(Extensions.ViewContainersRegistry).registerViewContainer(
{
id: LEGACY_AGENT_SESSIONS_VIEW_ID,
title: nls.localize2('chat.agent.sessions', "Agent Sessions"),
ctorDescriptor: new SyncDescriptor(ChatSessionsViewPaneContainer),
hideIfEmpty: true,
icon: registerIcon('chat-sessions-icon', Codicon.commentDiscussionSparkle, 'Icon for Agent Sessions View'),
order: 6
}, ViewContainerLocation.Sidebar);
}
}
export class ChatSessionsViewContrib extends Disposable implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.chatSessions';
private readonly registeredViewDescriptors: Map<string, IViewDescriptor> = new Map();
constructor(
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
@IProductService private readonly productService: IProductService,
) {
super();
// Initial check
void this.updateViewRegistration();
this._register(this.chatSessionsService.onDidChangeItemsProviders(() => {
void this.updateViewRegistration();
}));
this._register(this.chatSessionsService.onDidChangeAvailability(() => {
void this.updateViewRegistration();
}));
}
private getAllChatSessionItemProviders(): IChatSessionItemProvider[] {
return Array.from(this.chatSessionsService.getAllChatSessionItemProviders());
}
private async updateViewRegistration(): Promise<void> {
// prepare all chat session providers
const contributions = this.chatSessionsService.getAllChatSessionContributions();
await Promise.all(contributions.map(contrib => this.chatSessionsService.activateChatSessionItemProvider(contrib.type)));
const currentProviders = this.getAllChatSessionItemProviders();
const currentProviderIds = new Set(currentProviders.map(p => p.chatSessionType));
// Find views that need to be unregistered (providers that are no longer available)
const viewsToUnregister: IViewDescriptor[] = [];
for (const [providerId, viewDescriptor] of this.registeredViewDescriptors.entries()) {
if (!currentProviderIds.has(providerId)) {
viewsToUnregister.push(viewDescriptor);
this.registeredViewDescriptors.delete(providerId);
}
}
// Unregister removed views
if (viewsToUnregister.length > 0) {
const container = Registry.as<IViewContainersRegistry>(Extensions.ViewContainersRegistry).get(LEGACY_AGENT_SESSIONS_VIEW_ID);
if (container) {
Registry.as<IViewsRegistry>(Extensions.ViewsRegistry).deregisterViews(viewsToUnregister, container);
}
}
// Register new views
this.registerViews(contributions);
}
private async registerViews(extensionPointContributions: IChatSessionsExtensionPoint[]) {
const container = Registry.as<IViewContainersRegistry>(Extensions.ViewContainersRegistry).get(LEGACY_AGENT_SESSIONS_VIEW_ID);
const providers = this.getAllChatSessionItemProviders();
if (container && providers.length > 0) {
const viewDescriptorsToRegister: IViewDescriptor[] = [];
// Separate providers by type and prepare display names with order
const localProvider = providers.find(p => p.chatSessionType === localChatSessionType);
const historyProvider = providers.find(p => p.chatSessionType === 'history');
const otherProviders = providers.filter(p => p.chatSessionType !== localChatSessionType && p.chatSessionType !== 'history');
// Sort other providers by order, then alphabetically by display name
const providersWithDisplayNames = otherProviders.map(provider => {
const extContribution = extensionPointContributions.find(c => c.type === provider.chatSessionType);
if (!extContribution) {
return null;
}
return {
provider,
displayName: extContribution.displayName,
order: extContribution.order
};
}).filter(item => item !== null) as Array<{ provider: IChatSessionItemProvider; displayName: string; order: number | undefined }>;
providersWithDisplayNames.sort((a, b) => {
// Both have no order - sort by display name
if (a.order === undefined && b.order === undefined) {
return a.displayName.localeCompare(b.displayName);
}
// Only a has no order - push it to the end
if (a.order === undefined) {
return 1;
}
// Only b has no order - push it to the end
if (b.order === undefined) {
return -1;
}
// Both have orders - compare numerically
const orderCompare = a.order - b.order;
if (orderCompare !== 0) {
return orderCompare;
}
// Same order - sort by display name
return a.displayName.localeCompare(b.displayName);
});
// Register views in priority order: local, history, then alphabetically sorted others
const orderedProviders = [
...(localProvider ? [{ provider: localProvider, displayName: 'Local Chat Agent', baseOrder: 0, when: ChatContextKeyExprs.agentViewWhen }] : []),
...(historyProvider ? [{ provider: historyProvider, displayName: 'History', baseOrder: 1, when: ChatContextKeyExprs.agentViewWhen }] : []),
...providersWithDisplayNames.map((item, index) => ({
...item,
baseOrder: 2 + index, // Start from 2 for other providers
when: ChatContextKeyExprs.agentViewWhen,
}))
];
orderedProviders.forEach(({ provider, displayName, baseOrder, when }) => {
// Only register if not already registered
if (!this.registeredViewDescriptors.has(provider.chatSessionType)) {
const viewId = `${LEGACY_AGENT_SESSIONS_VIEW_ID}.${provider.chatSessionType}`;
const viewDescriptor: IViewDescriptor = {
id: viewId,
name: {
value: displayName,
original: displayName,
},
ctorDescriptor: new SyncDescriptor(SessionsViewPane, [provider, viewId]),
canToggleVisibility: true,
canMoveView: true,
order: baseOrder, // Use computed order based on priority and alphabetical sorting
when,
};
viewDescriptorsToRegister.push(viewDescriptor);
this.registeredViewDescriptors.set(provider.chatSessionType, viewDescriptor);
if (provider.chatSessionType === localChatSessionType) {
const viewsRegistry = Registry.as<IViewsRegistry>(Extensions.ViewsRegistry);
this._register(viewsRegistry.registerViewWelcomeContent(viewDescriptor.id, {
content: nls.localize('chatSessions.noResults', "No local chat agent sessions\n[Start an Agent Session](command:{0})", ACTION_ID_OPEN_CHAT),
}));
}
}
});
const gettingStartedViewId = `${LEGACY_AGENT_SESSIONS_VIEW_ID}.gettingStarted`;
if (!this.registeredViewDescriptors.has('gettingStarted')
&& this.productService.chatSessionRecommendations?.length) {
const gettingStartedDescriptor: IViewDescriptor = {
id: gettingStartedViewId,
name: {
value: nls.localize('chat.sessions.gettingStarted', "Getting Started"),
original: 'Getting Started',
},
ctorDescriptor: new SyncDescriptor(SessionsViewPane, [null, gettingStartedViewId]),
canToggleVisibility: true,
canMoveView: true,
order: 1000,
collapsed: !!otherProviders.length,
when: ContextKeyExpr.false()
};
viewDescriptorsToRegister.push(gettingStartedDescriptor);
this.registeredViewDescriptors.set('gettingStarted', gettingStartedDescriptor);
}
if (viewDescriptorsToRegister.length > 0) {
Registry.as<IViewsRegistry>(Extensions.ViewsRegistry).registerViews(viewDescriptorsToRegister, container);
}
}
}
override dispose(): void {
// Unregister all views before disposal
if (this.registeredViewDescriptors.size > 0) {
const container = Registry.as<IViewContainersRegistry>(Extensions.ViewContainersRegistry).get(LEGACY_AGENT_SESSIONS_VIEW_ID);
if (container) {
const allRegisteredViews = Array.from(this.registeredViewDescriptors.values());
Registry.as<IViewsRegistry>(Extensions.ViewsRegistry).deregisterViews(allRegisteredViews, container);
}
this.registeredViewDescriptors.clear();
}
super.dispose();
}
}
// Chat sessions container
class ChatSessionsViewPaneContainer extends ViewPaneContainer {
constructor(
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
@IContextMenuService contextMenuService: IContextMenuService,
@ITelemetryService telemetryService: ITelemetryService,
@IExtensionService extensionService: IExtensionService,
@IThemeService themeService: IThemeService,
@IStorageService storageService: IStorageService,
@IWorkspaceContextService contextService: IWorkspaceContextService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@ILogService logService: ILogService,
) {
super(
LEGACY_AGENT_SESSIONS_VIEW_ID,
{
mergeViewWithContainerWhenSingleView: false,
},
instantiationService,
configurationService,
layoutService,
contextMenuService,
telemetryService,
extensionService,
themeService,
storageService,
contextService,
viewDescriptorService,
logService
);
}
override getTitle(): string {
const title = nls.localize('chat.agent.sessions.title', "Agent Sessions");
return title;
}
}

View File

@@ -1,628 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DOM from '../../../../../../base/browser/dom.js';
import { $, append } from '../../../../../../base/browser/dom.js';
import { StandardKeyboardEvent } from '../../../../../../base/browser/keyboardEvent.js';
import { ActionBar } from '../../../../../../base/browser/ui/actionbar/actionbar.js';
import { HoverStyle } from '../../../../../../base/browser/ui/hover/hover.js';
import { HoverPosition } from '../../../../../../base/browser/ui/hover/hoverWidget.js';
import { IconLabel } from '../../../../../../base/browser/ui/iconLabel/iconLabel.js';
import { InputBox, MessageType } from '../../../../../../base/browser/ui/inputbox/inputBox.js';
import { IListRenderer, IListVirtualDelegate } from '../../../../../../base/browser/ui/list/list.js';
import { IAsyncDataSource, ITreeNode, ITreeRenderer } from '../../../../../../base/browser/ui/tree/tree.js';
import { timeout } from '../../../../../../base/common/async.js';
import { CancellationToken } from '../../../../../../base/common/cancellation.js';
import { Codicon } from '../../../../../../base/common/codicons.js';
import { FuzzyScore, createMatches } from '../../../../../../base/common/filters.js';
import { createSingleCallFunction } from '../../../../../../base/common/functional.js';
import { isMarkdownString } from '../../../../../../base/common/htmlContent.js';
import { KeyCode } from '../../../../../../base/common/keyCodes.js';
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js';
import { MarshalledId } from '../../../../../../base/common/marshallingIds.js';
import Severity from '../../../../../../base/common/severity.js';
import { ThemeIcon } from '../../../../../../base/common/themables.js';
import * as nls from '../../../../../../nls.js';
import { getActionBarActions } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js';
import { IMenuService, MenuId } from '../../../../../../platform/actions/common/actions.js';
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js';
import { IContextViewService } from '../../../../../../platform/contextview/browser/contextView.js';
import { IHoverService } from '../../../../../../platform/hover/browser/hover.js';
import { IMarkdownRendererService } from '../../../../../../platform/markdown/browser/markdownRenderer.js';
import product from '../../../../../../platform/product/common/product.js';
import { defaultInputBoxStyles } from '../../../../../../platform/theme/browser/defaultStyles.js';
import { IResourceLabel, ResourceLabels } from '../../../../../browser/labels.js';
import { IEditableData, ViewContainerLocation } from '../../../../../common/views.js';
import { IEditorGroupsService } from '../../../../../services/editor/common/editorGroupsService.js';
import { IWorkbenchLayoutService, Position } from '../../../../../services/layout/browser/layoutService.js';
import { getLocalHistoryDateFormatter } from '../../../../localHistory/browser/localHistory.js';
import { IChatService } from '../../../common/chatService.js';
import { ChatSessionStatus, IChatSessionItem, IChatSessionItemProvider, IChatSessionsService } from '../../../common/chatSessionsService.js';
import { LocalChatSessionUri } from '../../../common/chatUri.js';
import { ChatConfiguration } from '../../../common/constants.js';
import { IMarshalledChatSessionContext } from '../../actions/chatSessionActions.js';
import { allowedChatMarkdownHtmlTags } from '../../chatContentMarkdownRenderer.js';
import '../../media/chatSessions.css';
import { ChatSessionItemWithProvider, extractTimestamp, getSessionItemContextOverlay, processSessionsWithTimeGrouping } from '../common.js';
interface ISessionTemplateData {
readonly container: HTMLElement;
readonly iconLabel: IconLabel;
readonly actionBar: ActionBar;
readonly elementDisposable: DisposableStore;
readonly timestamp: HTMLElement;
readonly descriptionRow: HTMLElement;
readonly descriptionLabel: HTMLElement;
readonly statisticsLabel: HTMLElement;
readonly customIcon: HTMLElement;
}
export class ArchivedSessionItems {
private readonly items: Map<string, ChatSessionItemWithProvider> = new Map();
constructor(public readonly label: string) {
}
pushItem(item: ChatSessionItemWithProvider): void {
const key = item.resource.toString();
this.items.set(key, item);
}
getItems(): ChatSessionItemWithProvider[] {
return Array.from(this.items.values());
}
clear(): void {
this.items.clear();
}
}
export interface IGettingStartedItem {
id: string;
label: string;
commandId: string;
icon?: ThemeIcon;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args?: any[];
}
export class GettingStartedDelegate implements IListVirtualDelegate<IGettingStartedItem> {
getHeight(): number {
return 22;
}
getTemplateId(): string {
return 'gettingStartedItem';
}
}
interface IGettingStartedTemplateData {
resourceLabel: IResourceLabel;
}
export class GettingStartedRenderer implements IListRenderer<IGettingStartedItem, IGettingStartedTemplateData> {
readonly templateId = 'gettingStartedItem';
constructor(private readonly labels: ResourceLabels) { }
renderTemplate(container: HTMLElement): IGettingStartedTemplateData {
const resourceLabel = this.labels.create(container, { supportHighlights: true });
return { resourceLabel };
}
renderElement(element: IGettingStartedItem, index: number, templateData: IGettingStartedTemplateData): void {
templateData.resourceLabel.setResource({
name: element.label,
resource: undefined
}, {
icon: element.icon,
hideIcon: false
});
templateData.resourceLabel.element.setAttribute('data-command', element.commandId);
}
disposeTemplate(templateData: IGettingStartedTemplateData): void {
templateData.resourceLabel.dispose();
}
}
export class SessionsRenderer extends Disposable implements ITreeRenderer<IChatSessionItem, FuzzyScore, ISessionTemplateData> {
static readonly TEMPLATE_ID = 'session';
constructor(
private readonly viewLocation: ViewContainerLocation | null,
@IContextViewService private readonly contextViewService: IContextViewService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
@IMenuService private readonly menuService: IMenuService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IHoverService private readonly hoverService: IHoverService,
@IChatService private readonly chatService: IChatService,
@IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService,
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,
) {
super();
}
get templateId(): string {
return SessionsRenderer.TEMPLATE_ID;
}
private getHoverPosition(): HoverPosition {
const sideBarPosition = this.layoutService.getSideBarPosition();
switch (this.viewLocation) {
case ViewContainerLocation.Sidebar:
return sideBarPosition === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT;
case ViewContainerLocation.AuxiliaryBar:
return sideBarPosition === Position.LEFT ? HoverPosition.LEFT : HoverPosition.RIGHT;
default:
return HoverPosition.RIGHT;
}
}
renderTemplate(container: HTMLElement): ISessionTemplateData {
const element = append(container, $('.chat-session-item'));
// Create a container that holds the label, timestamp, and actions
const contentContainer = append(element, $('.session-content'));
// Custom icon element rendered separately from label text
const customIcon = append(contentContainer, $('.chat-session-custom-icon'));
const iconLabel = new IconLabel(contentContainer, { supportHighlights: true, supportIcons: true });
const descriptionRow = append(element, $('.description-row'));
const descriptionLabel = append(descriptionRow, $('span.description'));
const statisticsLabel = append(descriptionRow, $('span.statistics'));
// Create timestamp container and element
const timestampContainer = append(contentContainer, $('.timestamp-container'));
const timestamp = append(timestampContainer, $('.timestamp'));
const actionsContainer = append(contentContainer, $('.actions'));
const actionBar = new ActionBar(actionsContainer);
const elementDisposable = new DisposableStore();
return {
container: element,
iconLabel,
customIcon,
actionBar,
elementDisposable,
timestamp,
descriptionRow,
descriptionLabel,
statisticsLabel,
};
}
statusToIcon(status?: ChatSessionStatus) {
switch (status) {
case ChatSessionStatus.InProgress:
return ThemeIcon.modify(Codicon.loading, 'spin');
case ChatSessionStatus.Completed:
return Codicon.pass;
case ChatSessionStatus.Failed:
return Codicon.error;
default:
return Codicon.circleOutline;
}
}
private renderArchivedNode(node: ArchivedSessionItems, templateData: ISessionTemplateData): void {
templateData.customIcon.className = '';
templateData.descriptionRow.style.display = 'none';
templateData.timestamp.parentElement!.style.display = 'none';
const childCount = node.getItems().length;
templateData.iconLabel.setLabel(node.label, undefined, {
title: childCount === 1 ? nls.localize('chat.sessions.groupNode.single', '1 session') : nls.localize('chat.sessions.groupNode.multiple', '{0} sessions', childCount)
});
}
renderElement(element: ITreeNode<IChatSessionItem, FuzzyScore>, index: number, templateData: ISessionTemplateData): void {
if (element.element instanceof ArchivedSessionItems) {
this.renderArchivedNode(element.element, templateData);
return;
}
const session = element.element as ChatSessionItemWithProvider;
// Add CSS class for local sessions
let editableData: IEditableData | undefined;
if (LocalChatSessionUri.parseLocalSessionId(session.resource)) {
templateData.container.classList.add('local-session');
editableData = this.chatSessionsService.getEditableData(session.resource);
} else {
templateData.container.classList.remove('local-session');
}
// Check if this session is being edited using the actual session ID
if (editableData) {
// Render input box for editing
templateData.actionBar.clear();
const editDisposable = this.renderInputBox(templateData.container, session, editableData);
templateData.elementDisposable.add(editDisposable);
return;
}
// Normal rendering - clear the action bar in case it was used for editing
templateData.actionBar.clear();
// Handle different icon types
let iconTheme: ThemeIcon | undefined;
if (!session.iconPath) {
iconTheme = this.statusToIcon(session.status);
} else {
iconTheme = session.iconPath;
}
const renderDescriptionOnSecondRow = this.configurationService.getValue<boolean>(ChatConfiguration.ShowAgentSessionsViewDescription);
if (renderDescriptionOnSecondRow && session.description) {
templateData.container.classList.toggle('multiline', true);
templateData.descriptionRow.style.display = 'flex';
if (typeof session.description === 'string') {
templateData.descriptionLabel.textContent = session.description;
} else {
templateData.elementDisposable.add(this.markdownRendererService.render(session.description, {
sanitizerConfig: {
replaceWithPlaintext: true,
allowedTags: {
override: allowedChatMarkdownHtmlTags,
},
allowedLinkSchemes: { augment: [product.urlProtocol] }
},
}, templateData.descriptionLabel));
templateData.elementDisposable.add(DOM.addDisposableListener(templateData.descriptionLabel, 'mousedown', e => e.stopPropagation()));
templateData.elementDisposable.add(DOM.addDisposableListener(templateData.descriptionLabel, 'click', e => e.stopPropagation()));
templateData.elementDisposable.add(DOM.addDisposableListener(templateData.descriptionLabel, 'auxclick', e => e.stopPropagation()));
}
DOM.clearNode(templateData.statisticsLabel);
let insertions = 0;
let deletions = 0;
if (session.changes instanceof Array) {
for (const change of session.changes) {
insertions += change.insertions;
deletions += change.deletions;
}
} else if (session.changes) {
insertions = session.changes.insertions;
deletions = session.changes.deletions;
}
const insertionNode = append(templateData.statisticsLabel, $('span.insertions'));
insertionNode.textContent = session.changes ? `+${insertions}` : '';
const deletionNode = append(templateData.statisticsLabel, $('span.deletions'));
deletionNode.textContent = session.changes ? `-${deletions}` : '';
} else {
templateData.container.classList.toggle('multiline', false);
}
// Prepare tooltip content
const tooltipContent = 'tooltip' in session && session.tooltip ?
(typeof session.tooltip === 'string' ? session.tooltip :
isMarkdownString(session.tooltip) ? {
markdown: session.tooltip,
markdownNotSupportedFallback: session.tooltip.value
} : undefined) :
undefined;
templateData.customIcon.className = iconTheme ? `chat-session-custom-icon ${ThemeIcon.asClassName(iconTheme)}` : '';
// Set the icon label
templateData.iconLabel.setLabel(
session.label,
!renderDescriptionOnSecondRow && typeof session.description === 'string' ? session.description : undefined,
{
title: !renderDescriptionOnSecondRow || !session.description ? tooltipContent : undefined,
matches: createMatches(element.filterData)
}
);
// For two-row items, set tooltip on the container instead
if (renderDescriptionOnSecondRow && session.description && tooltipContent) {
if (typeof tooltipContent === 'string') {
templateData.elementDisposable.add(
this.hoverService.setupDelayedHover(templateData.container, () => ({
content: tooltipContent,
style: HoverStyle.Pointer,
position: { hoverPosition: this.getHoverPosition() }
}), { groupId: 'chat.sessions' })
);
} else if (tooltipContent && typeof tooltipContent === 'object' && 'markdown' in tooltipContent) {
templateData.elementDisposable.add(
this.hoverService.setupDelayedHover(templateData.container, () => ({
content: tooltipContent.markdown,
style: HoverStyle.Pointer,
position: { hoverPosition: this.getHoverPosition() }
}), { groupId: 'chat.sessions' })
);
}
}
// Handle timestamp display and grouping
const hasTimestamp = session.timing?.startTime !== undefined;
if (hasTimestamp) {
templateData.timestamp.textContent = session.relativeTime ?? '';
templateData.timestamp.ariaLabel = session.relativeTimeFullWord ?? '';
templateData.timestamp.parentElement!.classList.toggle('timestamp-duplicate', session.hideRelativeTime === true);
templateData.timestamp.parentElement!.style.display = '';
// Add tooltip showing full date/time when hovering over the timestamp
if (session.timing?.startTime) {
const fullDateTime = getLocalHistoryDateFormatter().format(session.timing.startTime);
templateData.elementDisposable.add(
this.hoverService.setupDelayedHover(templateData.timestamp, () => ({
content: nls.localize('chat.sessions.lastActivity', 'Last Activity: {0}', fullDateTime),
style: HoverStyle.Pointer,
position: { hoverPosition: this.getHoverPosition() }
}), { groupId: 'chat.sessions' })
);
}
} else {
// Hide timestamp container if no timestamp available
templateData.timestamp.parentElement!.style.display = 'none';
}
// Create context overlay for this specific session item
const contextOverlay = getSessionItemContextOverlay(
session,
session.provider,
this.chatService,
this.editorGroupsService
);
const contextKeyService = this.contextKeyService.createOverlay(contextOverlay);
// Create menu for this session item
const menu = templateData.elementDisposable.add(
this.menuService.createMenu(MenuId.AgentSessionsContext, contextKeyService)
);
// Setup action bar with contributed actions
const setupActionBar = () => {
templateData.actionBar.clear();
// Create marshalled context for command execution
const marshalledSession: IMarshalledChatSessionContext = {
session: session,
$mid: MarshalledId.ChatSessionContext
};
const actions = menu.getActions({ arg: marshalledSession, shouldForwardArgs: true });
const { primary } = getActionBarActions(
actions,
'inline',
);
templateData.actionBar.push(primary, { icon: true, label: false });
// Set context for the action bar
templateData.actionBar.context = session;
};
// Setup initial action bar and listen for menu changes
templateData.elementDisposable.add(menu.onDidChange(() => setupActionBar()));
setupActionBar();
}
disposeElement(_element: ITreeNode<IChatSessionItem, FuzzyScore>, _index: number, templateData: ISessionTemplateData): void {
templateData.elementDisposable.clear();
templateData.actionBar.clear();
}
private renderInputBox(container: HTMLElement, session: IChatSessionItem, editableData: IEditableData): DisposableStore {
// Hide the existing resource label element and session content
// eslint-disable-next-line no-restricted-syntax
const existingResourceLabelElement = container.querySelector('.monaco-icon-label') as HTMLElement;
if (existingResourceLabelElement) {
existingResourceLabelElement.style.display = 'none';
}
// Hide the session content container to avoid layout conflicts
// eslint-disable-next-line no-restricted-syntax
const sessionContentElement = container.querySelector('.session-content') as HTMLElement;
if (sessionContentElement) {
sessionContentElement.style.display = 'none';
}
// Create a simple container that mimics the file explorer's structure
const editContainer = DOM.append(container, DOM.$('.explorer-item.explorer-item-edited'));
// Add the icon
const iconElement = DOM.append(editContainer, DOM.$('.codicon'));
if (session.iconPath && ThemeIcon.isThemeIcon(session.iconPath)) {
iconElement.classList.add(`codicon-${session.iconPath.id}`);
} else {
iconElement.classList.add('codicon-file'); // Default file icon
}
// Create the input box directly
const inputBox = new InputBox(editContainer, this.contextViewService, {
validationOptions: {
validation: (value) => {
const message = editableData.validationMessage(value);
if (!message || message.severity !== Severity.Error) {
return null;
}
return {
content: message.content,
formatContent: true,
type: MessageType.ERROR
};
}
},
ariaLabel: nls.localize('chatSessionInputAriaLabel', "Type session name. Press Enter to confirm or Escape to cancel."),
inputBoxStyles: defaultInputBoxStyles,
});
inputBox.value = session.label;
inputBox.focus();
inputBox.select({ start: 0, end: session.label.length });
const done = createSingleCallFunction((success: boolean, finishEditing: boolean) => {
const value = inputBox.value;
// Clean up the edit container
editContainer.style.display = 'none';
editContainer.remove();
// Restore the original resource label
if (existingResourceLabelElement) {
existingResourceLabelElement.style.display = '';
}
// Restore the session content container
// eslint-disable-next-line no-restricted-syntax
const sessionContentElement = container.querySelector('.session-content') as HTMLElement;
if (sessionContentElement) {
sessionContentElement.style.display = '';
}
if (finishEditing) {
editableData.onFinish(value, success);
}
});
const showInputBoxNotification = () => {
if (inputBox.isInputValid()) {
const message = editableData.validationMessage(inputBox.value);
if (message) {
inputBox.showMessage({
content: message.content,
formatContent: true,
type: message.severity === Severity.Info ? MessageType.INFO : message.severity === Severity.Warning ? MessageType.WARNING : MessageType.ERROR
});
} else {
inputBox.hideMessage();
}
}
};
showInputBoxNotification();
const disposables: IDisposable[] = [
inputBox,
DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: StandardKeyboardEvent) => {
if (e.equals(KeyCode.Enter)) {
if (!inputBox.validate()) {
done(true, true);
}
} else if (e.equals(KeyCode.Escape)) {
done(false, true);
}
}),
DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_UP, () => {
showInputBoxNotification();
}),
DOM.addDisposableListener(inputBox.inputElement, DOM.EventType.BLUR, async () => {
while (true) {
await timeout(0);
const ownerDocument = inputBox.inputElement.ownerDocument;
if (!ownerDocument.hasFocus()) {
break;
}
if (DOM.isActiveElement(inputBox.inputElement)) {
return;
} else if (DOM.isHTMLElement(ownerDocument.activeElement) && DOM.hasParentWithClass(ownerDocument.activeElement, 'context-view')) {
// Do nothing - context menu is open
} else {
break;
}
}
done(inputBox.isInputValid(), true);
})
];
const disposableStore = new DisposableStore();
disposables.forEach(d => disposableStore.add(d));
disposableStore.add(toDisposable(() => done(false, false)));
return disposableStore;
}
disposeTemplate(templateData: ISessionTemplateData): void {
templateData.elementDisposable.dispose();
templateData.iconLabel.dispose();
templateData.actionBar.dispose();
}
}
// Chat sessions item data source for the tree
export class SessionsDataSource implements IAsyncDataSource<IChatSessionItemProvider, ChatSessionItemWithProvider | ArchivedSessionItems> {
// For now call it History until we support archive on all providers
private archivedItems = new ArchivedSessionItems(nls.localize('chat.sessions.archivedSessions', 'History'));
constructor(
private readonly provider: IChatSessionItemProvider,
) {
}
hasChildren(element: IChatSessionItemProvider | ChatSessionItemWithProvider | ArchivedSessionItems): boolean {
if (element === this.provider) {
// Root provider always has children
return true;
}
if (element instanceof ArchivedSessionItems) {
return element.getItems().length > 0;
}
return false;
}
async getChildren(element: IChatSessionItemProvider | ChatSessionItemWithProvider | ArchivedSessionItems): Promise<(ChatSessionItemWithProvider | ArchivedSessionItems)[]> {
if (element === this.provider) {
try {
const items = await this.provider.provideChatSessionItems(CancellationToken.None);
// Clear archived items from previous calls
this.archivedItems.clear();
const result: (ChatSessionItemWithProvider | ArchivedSessionItems)[] = items.map(item => {
const itemWithProvider = { ...item, provider: this.provider, timing: { startTime: extractTimestamp(item) ?? 0 } };
if (itemWithProvider.history) {
this.archivedItems.pushItem(itemWithProvider);
return;
}
return itemWithProvider;
}).filter(item => item !== undefined);
if (this.archivedItems.getItems().length > 0) {
result.push(this.archivedItems);
}
return result;
} catch (error) {
return [];
}
}
if (element instanceof ArchivedSessionItems) {
return processSessionsWithTimeGrouping(element.getItems());
}
// Individual session items don't have children
return [];
}
}
export class SessionsDelegate implements IListVirtualDelegate<ChatSessionItemWithProvider> {
static readonly ITEM_HEIGHT = 22;
static readonly ITEM_HEIGHT_WITH_DESCRIPTION = 44; // Slightly smaller for cleaner look
constructor(private readonly configurationService: IConfigurationService) { }
getHeight(element: ChatSessionItemWithProvider | ArchivedSessionItems): number {
// Return consistent height for all items (single-line layout)
if (this.configurationService.getValue(ChatConfiguration.ShowAgentSessionsViewDescription) && !(element instanceof ArchivedSessionItems) && element.description) {
return SessionsDelegate.ITEM_HEIGHT_WITH_DESCRIPTION;
} else {
return SessionsDelegate.ITEM_HEIGHT;
}
}
getTemplateId(element: ChatSessionItemWithProvider): string {
return SessionsRenderer.TEMPLATE_ID;
}
}

View File

@@ -1,511 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DOM from '../../../../../../base/browser/dom.js';
import { $, append } from '../../../../../../base/browser/dom.js';
import { renderAsPlaintext } from '../../../../../../base/browser/markdownRenderer.js';
import { IActionViewItem } from '../../../../../../base/browser/ui/actionbar/actionbar.js';
import { IBaseActionViewItemOptions } from '../../../../../../base/browser/ui/actionbar/actionViewItems.js';
import { ITreeContextMenuEvent } from '../../../../../../base/browser/ui/tree/tree.js';
import { IAction, toAction } from '../../../../../../base/common/actions.js';
import { Codicon } from '../../../../../../base/common/codicons.js';
import { FuzzyScore } from '../../../../../../base/common/filters.js';
import { MarshalledId } from '../../../../../../base/common/marshallingIds.js';
import { truncate } from '../../../../../../base/common/strings.js';
import { URI } from '../../../../../../base/common/uri.js';
import * as nls from '../../../../../../nls.js';
import { DropdownWithPrimaryActionViewItem } from '../../../../../../platform/actions/browser/dropdownWithPrimaryActionViewItem.js';
import { getActionBarActions } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js';
import { IMenuService, MenuId, MenuItemAction } from '../../../../../../platform/actions/common/actions.js';
import { ICommandService } from '../../../../../../platform/commands/common/commands.js';
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js';
import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js';
import { IHoverService } from '../../../../../../platform/hover/browser/hover.js';
import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js';
import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js';
import { WorkbenchAsyncDataTree, WorkbenchList } from '../../../../../../platform/list/browser/listService.js';
import { ILogService } from '../../../../../../platform/log/common/log.js';
import { IOpenerService } from '../../../../../../platform/opener/common/opener.js';
import { IProgressService } from '../../../../../../platform/progress/common/progress.js';
import { IThemeService } from '../../../../../../platform/theme/common/themeService.js';
import { fillEditorsDragData } from '../../../../../browser/dnd.js';
import { ResourceLabels } from '../../../../../browser/labels.js';
import { IViewPaneOptions, ViewPane } from '../../../../../browser/parts/views/viewPane.js';
import { IViewDescriptorService } from '../../../../../common/views.js';
import { IEditorGroupsService } from '../../../../../services/editor/common/editorGroupsService.js';
import { IChatService } from '../../../common/chatService.js';
import { IChatSessionItemProvider, IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js';
import { ChatConfiguration, ChatEditorTitleMaxLength } from '../../../common/constants.js';
import { ACTION_ID_OPEN_CHAT } from '../../actions/chatActions.js';
import { IMarshalledChatSessionContext } from '../../actions/chatSessionActions.js';
import { IChatWidgetService } from '../../chat.js';
import { IChatEditorOptions } from '../../chatEditor.js';
import { ChatSessionItemWithProvider, getSessionItemContextOverlay, NEW_CHAT_SESSION_ACTION_ID } from '../common.js';
import { ArchivedSessionItems, GettingStartedDelegate, GettingStartedRenderer, IGettingStartedItem, SessionsDataSource, SessionsDelegate, SessionsRenderer } from './sessionsTreeRenderer.js';
// Identity provider for session items
class SessionsIdentityProvider {
getId(element: ChatSessionItemWithProvider | ArchivedSessionItems): string {
if (element instanceof ArchivedSessionItems) {
return 'archived-session-items';
}
return element.resource.toString();
}
}
// Accessibility provider for session items
class SessionsAccessibilityProvider {
getWidgetAriaLabel(): string {
return nls.localize('chatSessions', 'Chat Sessions');
}
getAriaLabel(element: ChatSessionItemWithProvider | ArchivedSessionItems): string | null {
return element.label;
}
}
export class SessionsViewPane extends ViewPane {
private tree: WorkbenchAsyncDataTree<IChatSessionItemProvider, ChatSessionItemWithProvider, FuzzyScore> | undefined;
private list: WorkbenchList<IGettingStartedItem> | undefined;
private treeContainer: HTMLElement | undefined;
private messageElement?: HTMLElement;
private _isEmpty: boolean = true;
constructor(
private readonly provider: IChatSessionItemProvider,
private readonly viewId: string,
options: IViewPaneOptions,
@IKeybindingService keybindingService: IKeybindingService,
@IContextMenuService contextMenuService: IContextMenuService,
@IConfigurationService configurationService: IConfigurationService,
@IContextKeyService contextKeyService: IContextKeyService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@IInstantiationService instantiationService: IInstantiationService,
@IOpenerService openerService: IOpenerService,
@IThemeService themeService: IThemeService,
@IHoverService hoverService: IHoverService,
@IChatService private readonly chatService: IChatService,
@ILogService private readonly logService: ILogService,
@IProgressService private readonly progressService: IProgressService,
@IMenuService private readonly menuService: IMenuService,
@ICommandService private readonly commandService: ICommandService,
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
@IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService,
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
this.minimumBodySize = 44;
// Listen for configuration changes to refresh view when description display changes
this._register(this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(ChatConfiguration.ShowAgentSessionsViewDescription)) {
if (this.tree && this.isBodyVisible()) {
this.refreshTreeWithProgress();
}
}
}));
this._register(this.chatSessionsService.onDidChangeSessionItems((chatSessionType) => {
if (provider.chatSessionType === chatSessionType && this.tree && this.isBodyVisible()) {
this.refreshTreeWithProgress();
}
}));
if (provider) { // TODO: Why can this be undefined?
this.scopedContextKeyService.createKey('chatSessionType', provider.chatSessionType);
}
}
override shouldShowWelcome(): boolean {
return this._isEmpty;
}
public override createActionViewItem(action: IAction, options: IBaseActionViewItemOptions): IActionViewItem | undefined {
if (action.id.startsWith(NEW_CHAT_SESSION_ACTION_ID)) {
return this.getChatSessionDropdown(action, options);
}
return super.createActionViewItem(action, options);
}
private getChatSessionDropdown(defaultAction: IAction, options: IBaseActionViewItemOptions) {
const primaryAction = this.instantiationService.createInstance(MenuItemAction, {
id: defaultAction.id,
title: defaultAction.label,
icon: Codicon.plus,
}, undefined, undefined, undefined, undefined);
const actions = this.menuService.getMenuActions(MenuId.AgentSessionsContext, this.scopedContextKeyService, { shouldForwardArgs: true });
const primaryActions = getActionBarActions(
actions,
'submenu',
).primary.filter(action => {
if (action instanceof MenuItemAction && defaultAction instanceof MenuItemAction) {
if (!action.item.source?.id || !defaultAction.item.source?.id) {
return false;
}
if (action.item.source.id === defaultAction.item.source.id) {
return true;
}
}
return false;
});
if (!primaryActions || primaryActions.length === 0) {
return;
}
const dropdownAction = toAction({
id: 'selectNewChatSessionOption',
label: nls.localize('chatSession.selectOption', 'More...'),
class: 'codicon-chevron-down',
run: () => { }
});
const dropdownActions: IAction[] = [];
primaryActions.forEach(element => {
dropdownActions.push(element);
});
return this.instantiationService.createInstance(
DropdownWithPrimaryActionViewItem,
primaryAction,
dropdownAction,
dropdownActions,
'',
options
);
}
private isEmpty() {
// Check if the tree has the provider node and get its children count
if (!this.tree?.hasNode(this.provider)) {
return true;
}
const providerNode = this.tree.getNode(this.provider);
const childCount = providerNode.children?.length || 0;
return childCount === 0;
}
/**
* Updates the empty state message based on current tree data.
* Uses the tree's existing data to avoid redundant provider calls.
*/
private updateEmptyState(): void {
try {
const newEmptyState = this.isEmpty();
if (newEmptyState !== this._isEmpty) {
this._isEmpty = newEmptyState;
this._onDidChangeViewWelcomeState.fire();
}
} catch (error) {
this.logService.error('Error checking tree data for empty state:', error);
}
}
/**
* Refreshes the tree data with progress indication.
* Shows a progress indicator while the tree updates its children from the provider.
*/
private async refreshTreeWithProgress(): Promise<void> {
if (!this.tree) {
return;
}
try {
await this.progressService.withProgress(
{
location: this.id, // Use the view ID as the progress location
title: nls.localize('chatSessions.refreshing', 'Refreshing chat sessions...'),
},
async () => {
await this.tree!.updateChildren(this.provider);
}
);
// Check for empty state after refresh using tree data
this.updateEmptyState();
} catch (error) {
// Log error but don't throw to avoid breaking the UI
this.logService.error('Error refreshing chat sessions tree:', error);
}
}
/**
* Loads initial tree data with progress indication.
* Shows a progress indicator while the tree loads data from the provider.
*/
private async loadDataWithProgress(): Promise<void> {
if (!this.tree) {
return;
}
try {
await this.progressService.withProgress(
{
location: this.id, // Use the view ID as the progress location
title: nls.localize('chatSessions.loading', 'Loading chat sessions...'),
},
async () => {
await this.tree!.setInput(this.provider);
}
);
// Check for empty state after loading using tree data
this.updateEmptyState();
} catch (error) {
// Log error but don't throw to avoid breaking the UI
this.logService.error('Error loading chat sessions data:', error);
}
}
protected override renderBody(container: HTMLElement): void {
super.renderBody(container);
container.classList.add('chat-sessions-view');
// For Getting Started view (null provider), show simple list
if (this.provider === null) {
this.renderGettingStartedList(container);
return;
}
this.treeContainer = DOM.append(container, DOM.$('.chat-sessions-tree-container'));
// Create message element for empty state
this.messageElement = append(container, $('.chat-sessions-message'));
this.messageElement.style.display = 'none';
// Create the tree components
const dataSource = new SessionsDataSource(this.provider);
const delegate = new SessionsDelegate(this.configurationService);
const identityProvider = new SessionsIdentityProvider();
const accessibilityProvider = new SessionsAccessibilityProvider();
// Use the existing ResourceLabels service for consistent styling
const renderer = this.instantiationService.createInstance(SessionsRenderer, this.viewDescriptorService.getViewLocationById(this.viewId));
this._register(renderer);
const getResourceForElement = (element: ChatSessionItemWithProvider): URI => {
return element.resource;
};
this.tree = this.instantiationService.createInstance(
WorkbenchAsyncDataTree,
'ChatSessions',
this.treeContainer,
delegate,
[renderer],
dataSource,
{
dnd: {
onDragStart: (data, originalEvent) => {
try {
const elements = data.getData() as ChatSessionItemWithProvider[];
const uris = elements.map(getResourceForElement);
this.instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, uris, originalEvent));
} catch {
// noop
}
},
getDragURI: (element: ChatSessionItemWithProvider | ArchivedSessionItems) => {
if (element instanceof ArchivedSessionItems) {
return null;
}
return getResourceForElement(element).toString();
},
getDragLabel: (elements: ChatSessionItemWithProvider[]) => {
if (elements.length === 1) {
return elements[0].label;
}
return nls.localize('chatSessions.dragLabel', "{0} agent sessions", elements.length);
},
drop: () => { },
onDragOver: () => false,
dispose: () => { },
},
accessibilityProvider,
identityProvider,
keyboardNavigationLabelProvider: {
getKeyboardNavigationLabel: (session: ChatSessionItemWithProvider) => {
const parts = [
session.label || '',
typeof session.description === 'string' ? session.description : (session.description ? renderAsPlaintext(session.description) : '')
];
return parts.filter(text => text.length > 0).join(' ');
}
},
multipleSelectionSupport: false,
overrideStyles: {
listBackground: undefined
},
paddingBottom: SessionsDelegate.ITEM_HEIGHT,
setRowLineHeight: false
}
) as WorkbenchAsyncDataTree<IChatSessionItemProvider, ChatSessionItemWithProvider, FuzzyScore>;
// Set the input
this.tree.setInput(this.provider);
// Register tree events
this._register(this.tree.onDidOpen((e) => {
if (e.element) {
this.openChatSession(e.element);
}
}));
// Register context menu event for right-click actions
this._register(this.tree.onContextMenu((e) => {
if (e.element && !(e.element instanceof ArchivedSessionItems)) {
this.showContextMenu(e);
}
if (e.element) {
this.showContextMenu(e);
}
}));
this._register(this.tree.onMouseDblClick(e => {
const scrollingByPage = this.configurationService.getValue<boolean>('workbench.list.scrollByPage');
if (e.element === null && !scrollingByPage) {
if (this.provider?.chatSessionType && this.provider.chatSessionType !== localChatSessionType) {
this.commandService.executeCommand(`workbench.action.chat.openNewSessionEditor.${this.provider?.chatSessionType}`);
} else {
this.commandService.executeCommand(ACTION_ID_OPEN_CHAT);
}
}
}));
// Handle visibility changes to load data
this._register(this.onDidChangeBodyVisibility(async visible => {
if (visible && this.tree) {
await this.loadDataWithProgress();
}
}));
// Initially load data if visible
if (this.isBodyVisible() && this.tree) {
this.loadDataWithProgress();
}
this._register(this.tree);
}
private renderGettingStartedList(container: HTMLElement): void {
const listContainer = DOM.append(container, DOM.$('.getting-started-list-container'));
const items: IGettingStartedItem[] = [
{
id: 'install-extensions',
label: nls.localize('chatSessions.installExtensions', "Install Agents..."),
icon: Codicon.extensions,
commandId: 'chat.sessions.gettingStarted'
},
{
id: 'learn-more',
label: nls.localize('chatSessions.learnMoreGHCodingAgent', "Learn More About GitHub Copilot coding agent"),
commandId: 'vscode.open',
icon: Codicon.book,
args: [URI.parse('https://aka.ms/coding-agent-docs')]
}
];
const delegate = new GettingStartedDelegate();
// Create ResourceLabels instance for the renderer
const labels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility });
this._register(labels);
const renderer = new GettingStartedRenderer(labels);
this.list = this.instantiationService.createInstance(
WorkbenchList<IGettingStartedItem>,
'GettingStarted',
listContainer,
delegate,
[renderer],
{
horizontalScrolling: false,
}
);
this.list.splice(0, 0, items);
this._register(this.list.onDidOpen(e => {
if (e.element) {
this.commandService.executeCommand(e.element.commandId, ...e.element.args ?? []);
}
}));
this._register(this.list);
}
protected override layoutBody(height: number, width: number): void {
super.layoutBody(height, width);
if (this.tree) {
this.tree.layout(height, width);
}
if (this.list) {
this.list.layout(height, width);
}
}
private async openChatSession(session: ChatSessionItemWithProvider) {
try {
if (session instanceof ArchivedSessionItems) {
return;
}
const options: IChatEditorOptions = {
pinned: true,
ignoreInView: true,
title: {
preferred: truncate(session.label, ChatEditorTitleMaxLength),
},
preserveFocus: true,
};
await this.chatWidgetService.openSession(session.resource, undefined, options);
} catch (error) {
this.logService.error('[SessionsViewPane] Failed to open chat session:', error);
}
}
private showContextMenu(e: ITreeContextMenuEvent<ChatSessionItemWithProvider>) {
if (!e.element) {
return;
}
const session = e.element;
const sessionWithProvider = session;
// Create context overlay for this specific session item
const contextOverlay = getSessionItemContextOverlay(
session,
sessionWithProvider.provider,
this.chatService,
this.editorGroupsService
);
const contextKeyService = this.contextKeyService.createOverlay(contextOverlay);
// Create marshalled context for command execution
const marshalledSession: IMarshalledChatSessionContext = {
session: session,
$mid: MarshalledId.ChatSessionContext
};
// Create menu for this session item to get actions
const menu = this.menuService.createMenu(MenuId.AgentSessionsContext, contextKeyService);
// Get actions and filter for context menu (all actions that are NOT inline)
const actions = menu.getActions({ arg: marshalledSession, shouldForwardArgs: true });
const { secondary } = getActionBarActions(actions, 'inline');
this.contextMenuService.showContextMenu({
getActions: () => secondary,
getAnchor: () => e.anchor,
getActionsContext: () => marshalledSession,
});
menu.dispose();
}
}

View File

@@ -43,7 +43,6 @@ import { ChatContextKeys } from '../../common/chatContextKeys.js';
import { IChatModeService } from '../../common/chatModes.js';
import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js';
import { CHAT_CATEGORY, CHAT_SETUP_ACTION_ID, CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID } from '../actions/chatActions.js';
import { AGENT_SESSIONS_VIEW_CONTAINER_ID } from '../agentSessions/agentSessions.js';
import { ChatViewContainerId, IChatWidgetService } from '../chat.js';
import { chatViewsWelcomeRegistry } from '../viewsWelcome/chatViewsWelcome.js';
import { ChatSetupAnonymous } from './chatSetup.js';
@@ -707,12 +706,9 @@ export class ChatTeardownContribution extends Disposable implements IWorkbenchCo
const activeContainers = this.viewDescriptorService.getViewContainersByLocation(ViewContainerLocation.AuxiliaryBar).filter(
container => this.viewDescriptorService.getViewContainerModel(container).activeViewDescriptors.length > 0
);
const hasChatView = activeContainers.some(container => container.id === ChatViewContainerId);
const hasAgentSessionsView = activeContainers.some(container => container.id === AGENT_SESSIONS_VIEW_CONTAINER_ID);
if (
(activeContainers.length === 0) || // chat view is already gone but we know it was there before
(activeContainers.length === 1 && (hasChatView || hasAgentSessionsView)) || // chat view or agent sessions is the only view which is going to go away
(activeContainers.length === 2 && hasChatView && hasAgentSessionsView) // both chat and agent sessions view are going to go away
(activeContainers.length === 0) || // chat view is already gone but we know it was there before
(activeContainers.length === 1 && activeContainers.at(0)?.id === ChatViewContainerId) // chat view is the only view which is going to go away
) {
this.layoutService.setPartHidden(true, Parts.AUXILIARYBAR_PART); // hide if there are no views in the secondary sidebar
}

View File

@@ -40,13 +40,13 @@ import { EditorResourceAccessor, SideBySideEditor } from '../../../../common/edi
import { IChatEntitlementService, ChatEntitlementService, ChatEntitlement, IQuotaSnapshot } from '../../../../services/chat/common/chatEntitlementService.js';
import { IEditorService } from '../../../../services/editor/common/editorService.js';
import { IChatSessionsService } from '../../common/chatSessionsService.js';
import { openAgentSessionsView } from '../agentSessions/agentSessions.js';
import { isNewUser, isCompletionsEnabled } from './chatStatus.js';
import { IChatStatusItemService, ChatStatusEntry } from './chatStatusItemService.js';
import product from '../../../../../platform/product/common/product.js';
import { contrastBorder, inputValidationErrorBorder, inputValidationInfoBorder, inputValidationWarningBorder, registerColor, transparent } from '../../../../../platform/theme/common/colorRegistry.js';
import { Color } from '../../../../../base/common/color.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { IViewsService } from '../../../../services/views/common/viewsService.js';
import { ChatViewId } from '../chat.js';
const defaultChat = product.defaultChatAgent;
@@ -141,7 +141,7 @@ export class ChatStatusDashboard extends DomWidget {
@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IViewsService private readonly viewService: IViewsService,
) {
super();
@@ -227,7 +227,7 @@ export class ChatStatusDashboard extends DomWidget {
tooltip: localize('viewChatSessionsTooltip', "View Agent Sessions"),
class: ThemeIcon.asClassName(Codicon.eye),
run: () => {
this.instantiationService.invokeFunction(openAgentSessionsView);
this.viewService.openView(ChatViewId, true);
this.hoverService.hideHover(true);
}
}));

View File

@@ -55,6 +55,7 @@ import { ChatViewId } from './chat.js';
import { disposableTimeout } from '../../../../base/common/async.js';
import { AgentSessionsFilter } from './agentSessions/agentSessionsFilter.js';
import { IAgentSessionsService } from './agentSessions/agentSessionsService.js';
import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js';
interface IChatViewPaneState extends Partial<IChatModelInputState> {
sessionId?: string;
@@ -321,8 +322,8 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate {
// Sessions Control
this.sessionsControlContainer = append(sessionsContainer, $('.agent-sessions-control-container'));
const sessionsControl = this.sessionsControl = this._register(this.instantiationService.createInstance(AgentSessionsControl, this.sessionsControlContainer, {
allowOpenSessionsInPanel: true,
filter: sessionsFilter
filter: sessionsFilter,
getHoverPosition: () => this.sessionsViewerPosition === AgentSessionsViewerPosition.Right ? HoverPosition.LEFT : HoverPosition.RIGHT
}));
this._register(this.onDidChangeBodyVisibility(visible => sessionsControl.setVisible(visible)));

View File

@@ -1,299 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* Ensure consistent title background regardless of number of views */
.composite.viewlet[id="workbench.view.chat.sessions"] .pane-header.expanded.not-collapsible {
background-color: var(--vscode-sideBarSectionHeader-background) !important;
}
.chat-sessions-view {
display: flex;
flex-direction: column;
height: 100%;
}
.chat-sessions-tree-container,
.getting-started-list-container {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.chat-sessions-tree-container > .monaco-list,
.getting-started-list-container > .monaco-list {
flex: 1;
}
/* Style for empty state message */
.chat-sessions-message {
padding: 20px;
text-align: center;
color: var(--vscode-descriptionForeground);
}
.chat-sessions-message .no-sessions-message {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-style: italic;
}
/* Simple approach - directly style the edit container for chat sessions */
.chat-sessions-tree-container .explorer-item.explorer-item-edited {
display: flex;
align-items: center;
height: 22px;
padding: 0;
}
.chat-sessions-tree-container .explorer-item.explorer-item-edited .codicon {
margin-right: 6px;
flex-shrink: 0;
}
.chat-sessions-tree-container .explorer-item.explorer-item-edited .monaco-inputbox {
flex: 1;
width: 100%;
line-height: normal;
border: none !important;
background: transparent !important;
}
/* Add the complete outline border that file explorer uses (this replaces the border) */
.chat-sessions-tree-container .explorer-item.explorer-item-edited .monaco-inputbox input[type="text"] {
outline-width: 1px;
outline-style: solid;
outline-offset: -1px;
outline-color: var(--vscode-focusBorder);
opacity: 1;
border: none !important; /* Remove any default border */
}
.chat-sessions-tree-container .chat-session-item.multiline {
padding: 2px 0;
}
/* Position session content and actions inline */
.chat-sessions-tree-container .chat-session-item .session-content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
min-height: 22px;
line-height: 22px;
}
.chat-sessions-tree-container .chat-session-item .description-row {
display: none;
align-items: center;
font-size: 0.9em;
line-height: 1em;
margin: 2px 22px 0 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-sessions-tree-container .chat-session-item .description-row p {
padding: 2px;
margin: 0px;
border-radius: 4px;
}
.chat-sessions-tree-container .chat-session-item .description-row a {
color: var(--vscode-foreground);
}
.chat-sessions-tree-container .chat-session-item .description-row .description:hover p {
background: var(--vscode-toolbar-hoverBackground);
}
.chat-sessions-tree-container .chat-session-item .description-row .description {
opacity: 0.5;
}
.chat-sessions-tree-container .chat-session-item .description-row .description p {
display: flex;
align-items: center;
}
.chat-sessions-tree-container .chat-session-item .description-row .description p .codicon {
font-size: 14px;
}
.chat-sessions-tree-container .chat-session-item .description-row .statistics {
margin-left: 8px;
}
.getting-started-list-container .monaco-list-row {
padding-left: 8px;
}
.chat-sessions-tree-container .chat-session-item .description-row .statistics .insertions {
color: var(--vscode-chat-linesAddedForeground);
padding-left: 4px;
}
.chat-sessions-tree-container .chat-session-item .description-row .statistics .deletions {
color: var(--vscode-chat-linesRemovedForeground);
padding-left: 4px;
}
.chat-sessions-tree-container .chat-session-item .actions {
display: flex;
align-items: center;
flex-shrink: 0;
}
/* Hide actions by default, show on hover and focus */
.chat-sessions-tree-container .chat-session-item .actions .monaco-action-bar .action-label {
opacity: 0;
}
.chat-sessions-tree-container .chat-session-item:hover .actions .monaco-action-bar .action-label,
.chat-sessions-tree-container .monaco-list-row.focused .chat-session-item .actions .monaco-action-bar .action-label,
.chat-sessions-tree-container .monaco-list-row.selected .chat-session-item .actions .monaco-action-bar .action-label {
opacity: 1;
}
/* For items with descriptions, keep the structure but adjust alignment */
.chat-sessions-tree-container .chat-session-item .session-content {
align-items: center;
padding-top: 0;
padding-bottom: 0;
}
/* Ensure resource label takes up available space */
.chat-sessions-tree-container .chat-session-item .monaco-icon-label {
flex: 1;
min-width: 0; /* Allow text to truncate */
text-overflow: ellipsis;
overflow: hidden;
}
.chat-sessions-tree-container .chat-session-item .monaco-icon-label::before {
text-align: center;
}
/* Chat session icon */
.chat-sessions-tree-container .chat-session-item .chat-session-custom-icon {
flex-shrink: 0;
width: 16px;
height: 16px;
margin-right: 6px;
font-size: 16px;
display: flex;
}
/* Timestamp styling - similar to timeline pane */
.chat-sessions-tree-container .chat-session-item .timestamp-container {
margin-left: auto;
margin-right: 4px;
opacity: 0.5;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 0;
font-size: 0.9em;
min-width: 10px;
}
.chat-sessions-tree-container .chat-session-item .timestamp-container.timestamp-duplicate::before {
content: ' ';
position: absolute;
top: 0px;
right: 10px;
border-right: 1px solid currentColor;
display: block;
height: 100%;
width: 1px;
opacity: 0.25;
}
.chat-sessions-tree-container .monaco-list-row:hover .chat-session-item .timestamp-container.timestamp-duplicate::before,
.chat-sessions-tree-container .monaco-list-row.selected .chat-session-item .timestamp-container.timestamp-duplicate::before,
.chat-sessions-tree-container .monaco-list-row.focused .chat-session-item .timestamp-container.timestamp-duplicate::before {
display: none;
}
.chat-sessions-tree-container .chat-session-item .timestamp-container .timestamp {
display: inline-block;
}
.chat-sessions-tree-container .chat-session-item .timestamp-container.timestamp-duplicate .timestamp {
visibility: hidden;
width: 10px;
}
.chat-sessions-tree-container .monaco-list-row:hover .chat-session-item .timestamp-container.timestamp-duplicate .timestamp,
.chat-sessions-tree-container .monaco-list-row.selected .chat-session-item .timestamp-container.timestamp-duplicate .timestamp,
.chat-sessions-tree-container .monaco-list-row.focused .chat-session-item .timestamp-container.timestamp-duplicate .timestamp {
visibility: visible !important;
width: initial;
}
.chat-sessions-tree-container .monaco-list-row .actions {
display: none;
}
.chat-sessions-tree-container .monaco-list-row:hover .actions {
display: block;
}
/* Hide twisties for elements that don't have children */
.chat-sessions-tree-container .monaco-list-row .monaco-tl-twistie {
/* Ultra-small indent to separate parent/child without large gutter */
visibility: hidden; /* keep layout space */
width: 3px;
min-width: 3px;
padding: 0;
margin: 0;
}
/* Show twistie only for collapsible items (like "Show history...") */
.chat-sessions-tree-container .monaco-list-row[aria-expanded] .monaco-tl-twistie {
visibility: visible;
width: auto;
padding-left: 0px;
padding-right: 6px;
margin: initial;
}
/* History items styling */
.chat-sessions-tree-container .chat-session-item[data-history-item="true"] {
opacity: 0.9;
}
.chat-sessions-tree-container .chat-session-item[data-history-item="true"]:hover {
background-color: var(--vscode-list-hoverBackground);
}
/* Chat editor relative positioning for loading overlay */
.chat-editor-relative {
position: relative;
}
/* Chat editor loading overlay styles */
.chat-loading-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--vscode-editor-background);
z-index: 1000;
}
.chat-loading-overlay .chat-loading-content {
display: flex;
align-items: center;
gap: 8px;
color: var(--vscode-editor-foreground);
}
.chat-loading-overlay .codicon {
font-size: 16px;
}

View File

@@ -9,7 +9,7 @@ import { IsWebContext } from '../../../../platform/contextkey/common/contextkeys
import { RemoteNameContext } from '../../../common/contextkeys.js';
import { ViewContainerLocation } from '../../../common/views.js';
import { ChatEntitlementContextKeys } from '../../../services/chat/common/chatEntitlementService.js';
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from './constants.js';
import { ChatAgentLocation, ChatModeKind } from './constants.js';
export namespace ChatContextKeys {
export const responseVote = new RawContextKey<string>('chatSessionResponseVote', '', { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") });
@@ -92,15 +92,13 @@ export namespace ChatContextKeys {
export const panelLocation = new RawContextKey<ViewContainerLocation>('chatPanelLocation', undefined, { type: 'number', description: localize('chatPanelLocation', "The location of the chat panel.") });
export const isCombinedAgentSessionsViewer = new RawContextKey<boolean>('chatIsCombinedSessionViewer', false, { type: 'boolean', description: localize('chatIsCombinedSessionViewer', "True when the chat session viewer uses the new combined style.") }); // TODO@bpasero eventually retire this context key
export const agentSessionsViewerLimited = new RawContextKey<boolean>('agentSessionsViewerLimited', undefined, { type: 'boolean', description: localize('agentSessionsViewerLimited', "If the agent sessions view in the chat view is limited to show recent sessions only.") });
export const agentSessionsViewerOrientation = new RawContextKey<number>('agentSessionsViewerOrientation', undefined, { type: 'number', description: localize('agentSessionsViewerOrientation', "Orientation of the agent sessions view in the chat view.") });
export const agentSessionsViewerPosition = new RawContextKey<number>('agentSessionsViewerPosition', undefined, { type: 'number', description: localize('agentSessionsViewerPosition', "Position of the agent sessions view in the chat view.") });
export const agentSessionType = new RawContextKey<string>('chatSessionType', '', { type: 'string', description: localize('agentSessionType', "The type of the current agent session item.") });
export const hasAgentSessionChanges = new RawContextKey<boolean>('agentSessionHasChanges', false, { type: 'boolean', description: localize('agentSessionHasChanges', "True when the current agent session item has changes.") });
export const isArchivedAgentSession = new RawContextKey<boolean>('agentSessionIsArchived', false, { type: 'boolean', description: localize('agentSessionIsArchived', "True when the agent session item is archived.") });
export const isReadAgentSession = new RawContextKey<boolean>('agentSessionIsRead', false, { type: 'boolean', description: localize('agentSessionIsRead', "True when the agent session item is read.") });
export const isActiveAgentSession = new RawContextKey<boolean>('agentSessionIsActive', false, { type: 'boolean', description: localize('agentSessionIsActive', "True when the agent session is currently active (not deletable).") });
export const hasAgentSessionChanges = new RawContextKey<boolean>('agentSessionHasChanges', false, { type: 'boolean', description: localize('agentSessionHasChanges', "True when the current agent session item has changes.") });
export const isKatexMathElement = new RawContextKey<boolean>('chatIsKatexMathElement', false, { type: 'boolean', description: localize('chatIsKatexMathElement', "True when focusing a KaTeX math element.") });
}
@@ -118,9 +116,4 @@ export namespace ChatContextKeyExprs {
ChatContextKeys.Setup.installed.negate(),
ChatContextKeys.Entitlement.canSignUp
);
export const agentViewWhen = ContextKeyExpr.and(
ChatEntitlementContextKeys.Setup.hidden.negate(),
ChatEntitlementContextKeys.Setup.disabled.negate(),
ContextKeyExpr.equals(`config.${ChatConfiguration.AgentSessionsViewLocation}`, 'view'));
}

View File

@@ -11,7 +11,6 @@ import { IObservable } from '../../../../base/common/observable.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { URI } from '../../../../base/common/uri.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IEditableData } from '../../../common/views.js';
import { IChatAgentAttachmentCapabilities, IChatAgentRequest } from './chatAgents.js';
import { IChatEditingSession } from './chatEditingService.js';
import { IChatModel, IChatRequestVariableData, ISerializableChatModelInputState } from './chatModel.js';
@@ -225,11 +224,6 @@ export interface IChatSessionsService {
setOptionsChangeCallback(callback: SessionOptionsChangedCallback): void;
notifySessionOptionsChange(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>): Promise<void>;
// Editable session support
setEditableSession(sessionResource: URI, data: IEditableData | null): Promise<void>;
getEditableData(sessionResource: URI): IEditableData | undefined;
isEditable(sessionResource: URI): boolean;
// #endregion
registerChatModelChangeListeners(chatService: IChatService, chatSessionType: string, onChange: () => void): IDisposable;
getInProgressSessionDescription(chatModel: IChatModel): string | undefined;
}

View File

@@ -19,11 +19,9 @@ export enum ChatConfiguration {
EligibleForAutoApproval = 'chat.tools.eligibleForAutoApproval',
EnableMath = 'chat.math.enabled',
CheckpointsEnabled = 'chat.checkpoints.enabled',
AgentSessionsViewLocation = 'chat.agentSessionsViewLocation',
ThinkingStyle = 'chat.agent.thinkingStyle',
ThinkingGenerateTitles = 'chat.agent.thinking.generateTitles',
TodosShowWidget = 'chat.tools.todos.showWidget',
ShowAgentSessionsViewDescription = 'chat.showAgentSessionsViewDescription',
NotifyWindowOnResponseReceived = 'chat.notifyWindowOnResponseReceived',
ChatViewSessionsEnabled = 'chat.viewSessions.enabled',
ChatViewSessionsOrientation = 'chat.viewSessions.orientation',
@@ -133,8 +131,6 @@ export function isSupportedChatFileScheme(accessor: ServicesAccessor, scheme: st
return true;
}
/** @deprecated */
export const LEGACY_AGENT_SESSIONS_VIEW_ID = 'workbench.view.chat.sessions'; // TODO@bpasero clear once settled
export const MANAGE_CHAT_COMMAND_ID = 'workbench.action.chat.manage';
export const ChatEditorTitleMaxLength = 30;

View File

@@ -9,7 +9,6 @@ import { IDisposable } from '../../../../../base/common/lifecycle.js';
import { ResourceMap } from '../../../../../base/common/map.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { URI } from '../../../../../base/common/uri.js';
import { IEditableData } from '../../../../common/views.js';
import { IChatAgentAttachmentCapabilities } from '../../common/chatAgents.js';
import { IChatModel } from '../../common/chatModel.js';
import { IChatService } from '../../common/chatService.js';
@@ -40,7 +39,6 @@ export class MockChatSessionsService implements IChatSessionsService {
private contributions: IChatSessionsExtensionPoint[] = [];
private optionGroups = new Map<string, IChatSessionProviderOptionGroup[]>();
private sessionOptions = new ResourceMap<Map<string, string>>();
private editableData = new ResourceMap<IEditableData>();
private inProgress = new Map<string, number>();
private onChange = () => { };
@@ -173,22 +171,6 @@ export class MockChatSessionsService implements IChatSessionsService {
await this.optionsChangeCallback?.(sessionResource, updates);
}
async setEditableSession(sessionResource: URI, data: IEditableData | null): Promise<void> {
if (data) {
this.editableData.set(sessionResource, data);
} else {
this.editableData.delete(sessionResource);
}
}
getEditableData(sessionResource: URI): IEditableData | undefined {
return this.editableData.get(sessionResource);
}
isEditable(sessionResource: URI): boolean {
return this.editableData.has(sessionResource);
}
notifySessionItemsChanged(chatSessionType: string): void {
this._onDidChangeSessionItems.fire(chatSessionType);
}