From a2ad5acb046ef375c9f65af1483d4348cdf84836 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 19 Jan 2026 11:41:52 +0100 Subject: [PATCH] agent sessions - introduce experiments and adopt for projection & status (#288696) --- .github/CODENOTIFY | 2 - src/vs/platform/actions/common/actions.ts | 2 +- .../titlebar/commandCenterControlRegistry.ts | 71 ---------- .../browser/parts/titlebar/titlebarPart.ts | 30 +--- .../chat/browser/actions/chatActions.ts | 4 - .../chat/browser/actions/chatNewActions.ts | 8 -- .../agentSessions.contribution.ts | 92 +----------- .../browser/agentSessions/agentSessions.ts | 4 +- .../agentSessions/agentSessionsControl.ts | 23 ++- .../agentSessions/agentSessionsOpener.ts | 69 ++++++--- .../experiments/agentSessionProjection.ts | 9 ++ .../agentSessionProjectionActions.ts | 33 ++--- .../agentSessionProjectionService.ts | 73 ++++++---- .../agentSessionsExperiments.contribution.ts | 48 +++++++ .../agentTitleBarStatusService.ts} | 12 +- .../agentTitleBarStatusWidget.ts} | 133 ++++++++++++------ .../media/agentsessionprojection.css} | 4 - .../media/agenttitlebarstatuswidget.css} | 4 - .../chat/browser/widget/chatWidgetService.ts | 5 - .../browser/widgetHosts/editor/chatEditor.ts | 1 - .../widgetHosts/viewPane/chatViewPane.ts | 4 +- .../chat/common/actions/chatContextKeys.ts | 4 - .../browser/agentSessionsWelcome.ts | 7 +- 23 files changed, 293 insertions(+), 349 deletions(-) delete mode 100644 src/vs/workbench/browser/parts/titlebar/commandCenterControlRegistry.ts create mode 100644 src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjection.ts rename src/vs/workbench/contrib/chat/browser/agentSessions/{ => experiments}/agentSessionProjectionActions.ts (74%) rename src/vs/workbench/contrib/chat/browser/agentSessions/{ => experiments}/agentSessionProjectionService.ts (81%) create mode 100644 src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionsExperiments.contribution.ts rename src/vs/workbench/contrib/chat/browser/agentSessions/{agentStatusService.ts => experiments/agentTitleBarStatusService.ts} (86%) rename src/vs/workbench/contrib/chat/browser/agentSessions/{agentStatusWidget.ts => experiments/agentTitleBarStatusWidget.ts} (84%) rename src/vs/workbench/contrib/chat/browser/agentSessions/{media/agentSessionProjection.css => experiments/media/agentsessionprojection.css} (94%) rename src/vs/workbench/contrib/chat/browser/agentSessions/{media/agentStatusWidget.css => experiments/media/agenttitlebarstatuswidget.css} (98%) diff --git a/.github/CODENOTIFY b/.github/CODENOTIFY index ac22ac40d26..dc4ef34cf21 100644 --- a/.github/CODENOTIFY +++ b/.github/CODENOTIFY @@ -7,7 +7,6 @@ src/vs/base/common/path.ts @bpasero src/vs/base/common/stream.ts @bpasero src/vs/base/common/uri.ts @jrieken src/vs/base/browser/domSanitize.ts @mjbvz -src/vs/base/browser/** @bpasero src/vs/base/node/pfs.ts @bpasero src/vs/base/node/unc.ts @bpasero src/vs/base/parts/contextmenu/** @bpasero @@ -110,7 +109,6 @@ src/vs/workbench/contrib/chat/browser/media/chatViewTitleControl.css @bpasero src/vs/workbench/contrib/chat/browser/chatManagement/chatUsageWidget.ts @bpasero src/vs/workbench/contrib/chat/browser/chatManagement/media/chatUsageWidget.css @bpasero src/vs/workbench/contrib/chat/browser/agentSessions/** @bpasero -src/vs/workbench/contrib/chat/browser/chatSessions/** @bpasero src/vs/workbench/contrib/localization/** @TylerLeonhardt src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @TylerLeonhardt src/vs/workbench/contrib/scm/** @lszomoru diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index a66127dd044..5c8cd932f8e 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -292,7 +292,7 @@ export class MenuId { static readonly AgentSessionsToolbar = new MenuId('AgentSessionsToolbar'); static readonly AgentSessionItemToolbar = new MenuId('AgentSessionItemToolbar'); static readonly AgentSessionSectionToolbar = new MenuId('AgentSessionSectionToolbar'); - static readonly AgentsControlMenu = new MenuId('AgentsControlMenu'); + static readonly AgentsTitleBarControlMenu = new MenuId('AgentsTitleBarControlMenu'); static readonly ChatViewSessionTitleNavigationToolbar = new MenuId('ChatViewSessionTitleNavigationToolbar'); static readonly ChatViewSessionTitleToolbar = new MenuId('ChatViewSessionTitleToolbar'); diff --git a/src/vs/workbench/browser/parts/titlebar/commandCenterControlRegistry.ts b/src/vs/workbench/browser/parts/titlebar/commandCenterControlRegistry.ts deleted file mode 100644 index 20eeafacdb0..00000000000 --- a/src/vs/workbench/browser/parts/titlebar/commandCenterControlRegistry.ts +++ /dev/null @@ -1,71 +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 { IDisposable } from '../../../../base/common/lifecycle.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; - -/** - * Interface for a command center control that can be registered with the titlebar. - */ -export interface ICommandCenterControl extends IDisposable { - readonly element: HTMLElement; -} - -/** - * A registration for a custom command center control. - */ -export interface ICommandCenterControlRegistration { - /** - * The context key that must be truthy for this control to be shown. - * When this context key is true, this control replaces the default command center. - */ - readonly contextKey: string; - - /** - * Priority for when multiple controls match. Higher priority wins. - */ - readonly priority: number; - - /** - * Factory function to create the control. - */ - create(instantiationService: IInstantiationService): ICommandCenterControl; -} - -class CommandCenterControlRegistryImpl { - private readonly registrations: ICommandCenterControlRegistration[] = []; - - /** - * Register a custom command center control. - */ - register(registration: ICommandCenterControlRegistration): IDisposable { - this.registrations.push(registration); - // Sort by priority descending - this.registrations.sort((a, b) => b.priority - a.priority); - - return { - dispose: () => { - const index = this.registrations.indexOf(registration); - if (index >= 0) { - this.registrations.splice(index, 1); - } - } - }; - } - - /** - * Get all registered command center controls. - */ - getRegistrations(): readonly ICommandCenterControlRegistration[] { - return this.registrations; - } -} - -/** - * Registry for custom command center controls. - * Contrib modules can register controls here, and the titlebar will use them - * when their context key conditions are met. - */ -export const CommandCenterControlRegistry = new CommandCenterControlRegistryImpl(); diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 831c4be2380..743f9e6ee8b 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -30,7 +30,6 @@ import { IContextKey, IContextKeyService } from '../../../../platform/contextkey import { IHostService } from '../../../services/host/browser/host.js'; import { WindowTitle } from './windowTitle.js'; import { CommandCenterControl } from './commandCenterControl.js'; -import { CommandCenterControlRegistry } from './commandCenterControlRegistry.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; import { WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { ACCOUNTS_ACTIVITY_ID, GLOBAL_ACTIVITY_ID } from '../../../common/activity.js'; @@ -329,14 +328,6 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { this._register(this.hostService.onDidChangeActiveWindow(windowId => windowId === targetWindowId ? this.onFocus() : this.onBlur())); this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChanged(e))); this._register(this.editorGroupsContainer.onDidChangeEditorPartOptions(e => this.onEditorPartConfigurationChange(e))); - - // Re-create title when any registered command center control's context key changes - this._register(this.contextKeyService.onDidChangeContext(e => { - const registeredContextKeys = new Set(CommandCenterControlRegistry.getRegistrations().map(r => r.contextKey)); - if (registeredContextKeys.size > 0 && e.affectsSome(registeredContextKeys)) { - this.createTitle(); - } - })); } private onBlur(): void { @@ -585,24 +576,9 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { // Menu Title else { - // Check if any registered command center control should be shown - let customControlShown = false; - for (const registration of CommandCenterControlRegistry.getRegistrations()) { - if (this.contextKeyService.getContextKeyValue(registration.contextKey)) { - const control = registration.create(this.instantiationService); - reset(this.title, control.element); - this.titleDisposables.add(control); - customControlShown = true; - break; - } - } - - if (!customControlShown) { - // Normal mode - show regular command center - const commandCenter = this.instantiationService.createInstance(CommandCenterControl, this.windowTitle, this.hoverDelegate); - reset(this.title, commandCenter.element); - this.titleDisposables.add(commandCenter); - } + const commandCenter = this.instantiationService.createInstance(CommandCenterControl, this.windowTitle, this.hoverDelegate); + reset(this.title, commandCenter.element); + this.titleDisposables.add(commandCenter); } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index c6b4113cbd4..8b6a6f5f946 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -948,10 +948,6 @@ MenuRegistry.appendMenuItem(MenuId.CommandCenter, { ChatContextKeys.Setup.disabled.negate() ), ContextKeyExpr.has('config.chat.commandCenter.enabled'), - ContextKeyExpr.or( - ContextKeyExpr.has(`config.${ChatConfiguration.AgentStatusEnabled}`).negate(), // Show when agent status is disabled - ChatContextKeys.agentStatusHasNotifications.negate() // Or when agent status has no notifications - ) ), order: 10003 // to the right of agent controls }); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts index 1c525a8eaa0..d662fd086eb 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts @@ -30,7 +30,6 @@ import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js'; import { ACTION_ID_NEW_CHAT, ACTION_ID_NEW_EDIT_SESSION, CHAT_CATEGORY, handleCurrentEditingSession } from './chatActions.js'; import { clearChatEditor } from './chatClear.js'; import { AgentSessionsViewerOrientation } from '../agentSessions/agentSessions.js'; -import { IAgentSessionProjectionService } from '../agentSessions/agentSessionProjectionService.js'; export interface INewEditSessionActionContext { @@ -121,13 +120,6 @@ export function registerNewChatActions() { async run(accessor: ServicesAccessor, ...args: unknown[]) { const accessibilityService = accessor.get(IAccessibilityService); - const projectionService = accessor.get(IAgentSessionProjectionService); - - // Exit projection mode if active (back button behavior) - if (projectionService.isActive) { - await projectionService.exitProjection(); - return; - } const viewsService = accessor.get(IViewsService); const executeCommandContext = args[0] as INewEditSessionActionContext | undefined; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts index 10c3927eab9..1016c2afb42 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.contribution.ts @@ -3,10 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import './experiments/agentSessionsExperiments.contribution.js'; import { Codicon } from '../../../../../base/common/codicons.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; import { localize, localize2 } from '../../../../../nls.js'; -import { mainWindow } from '../../../../../base/browser/window.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { registerSingleton, InstantiationType } from '../../../../../platform/instantiation/common/extensions.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; @@ -15,20 +14,11 @@ import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { AgentSessionsViewerOrientation, AgentSessionsViewerPosition } from './agentSessions.js'; import { IAgentSessionsService, AgentSessionsService } from './agentSessionsService.js'; import { LocalAgentsSessionsProvider } from './localAgentSessionsProvider.js'; -import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../common/contributions.js'; -import { ISubmenuItem, MenuId, MenuRegistry, registerAction2, SubmenuItemAction } from '../../../../../platform/actions/common/actions.js'; -import { ArchiveAgentSessionAction, ArchiveAgentSessionSectionAction, UnarchiveAgentSessionSectionAction, UnarchiveAgentSessionAction, OpenAgentSessionInEditorGroupAction, OpenAgentSessionInNewEditorGroupAction, OpenAgentSessionInNewWindowAction, ShowAgentSessionsSidebar, HideAgentSessionsSidebar, ToggleAgentSessionsSidebar, RefreshAgentSessionsViewerAction, FindAgentSessionInViewerAction, MarkAgentSessionUnreadAction, MarkAgentSessionReadAction, MarkAgentSessionSectionReadAction, FocusAgentSessionsAction, SetAgentSessionsOrientationStackedAction, SetAgentSessionsOrientationSideBySideAction, ShowAllAgentSessionsAction, ShowRecentAgentSessionsAction, HideAgentSessionsAction, PickAgentSessionAction, ArchiveAllAgentSessionsAction, RenameAgentSessionAction, DeleteAgentSessionAction, DeleteAllLocalSessionsAction } from './agentSessionsActions.js'; +import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../common/contributions.js'; +import { ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { ArchiveAgentSessionAction, ArchiveAgentSessionSectionAction, UnarchiveAgentSessionAction, OpenAgentSessionInEditorGroupAction, OpenAgentSessionInNewEditorGroupAction, OpenAgentSessionInNewWindowAction, ShowAgentSessionsSidebar, HideAgentSessionsSidebar, ToggleAgentSessionsSidebar, RefreshAgentSessionsViewerAction, FindAgentSessionInViewerAction, MarkAgentSessionUnreadAction, MarkAgentSessionReadAction, FocusAgentSessionsAction, SetAgentSessionsOrientationStackedAction, SetAgentSessionsOrientationSideBySideAction, PickAgentSessionAction, ArchiveAllAgentSessionsAction, RenameAgentSessionAction, DeleteAgentSessionAction, DeleteAllLocalSessionsAction, HideAgentSessionsAction, MarkAgentSessionSectionReadAction, ShowAllAgentSessionsAction, ShowRecentAgentSessionsAction, UnarchiveAgentSessionSectionAction } from './agentSessionsActions.js'; import { AgentSessionsQuickAccessProvider, AGENT_SESSIONS_QUICK_ACCESS_PREFIX } from './agentSessionsQuickAccess.js'; -import { IAgentSessionProjectionService, AgentSessionProjectionService } from './agentSessionProjectionService.js'; -import { EnterAgentSessionProjectionAction, ExitAgentSessionProjectionAction, ToggleAgentStatusAction, ToggleAgentSessionProjectionAction } from './agentSessionProjectionActions.js'; -import { IAgentStatusService, AgentStatusService } from './agentStatusService.js'; -import { AgentStatusWidget } from './agentStatusWidget.js'; -import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { ChatConfiguration } from '../../common/constants.js'; import { AuxiliaryBarMaximizedContext } from '../../../../common/contextkeys.js'; -import { LayoutSettings } from '../../../../services/layout/browser/layoutService.js'; //#region Actions and Menus @@ -59,12 +49,6 @@ registerAction2(HideAgentSessionsAction); registerAction2(SetAgentSessionsOrientationStackedAction); registerAction2(SetAgentSessionsOrientationSideBySideAction); -// Agent Session Projection -registerAction2(EnterAgentSessionProjectionAction); -registerAction2(ExitAgentSessionProjectionAction); -registerAction2(ToggleAgentStatusAction); -registerAction2(ToggleAgentSessionProjectionAction); - // --- Agent Sessions Toolbar MenuRegistry.appendMenuItem(MenuId.AgentSessionsToolbar, { @@ -193,73 +177,7 @@ Registry.as(QuickAccessExtensions.Quickaccess).registerQui //#region Workbench Contributions registerWorkbenchContribution2(LocalAgentsSessionsProvider.ID, LocalAgentsSessionsProvider, WorkbenchPhase.AfterRestored); + registerSingleton(IAgentSessionsService, AgentSessionsService, InstantiationType.Delayed); -registerSingleton(IAgentStatusService, AgentStatusService, InstantiationType.Delayed); -registerSingleton(IAgentSessionProjectionService, AgentSessionProjectionService, InstantiationType.Delayed); - -// Register Agent Status as a menu item in the command center (alongside the search box, not replacing it) -MenuRegistry.appendMenuItem(MenuId.CommandCenter, { - submenu: MenuId.AgentsControlMenu, - title: localize('agentsControl', "Agents"), - icon: Codicon.chatSparkle, - when: ContextKeyExpr.has(`config.${ChatConfiguration.AgentStatusEnabled}`), - order: 10002 // to the right of the chat button -}); - -// Register a placeholder action to the submenu so it appears (required for submenus) -MenuRegistry.appendMenuItem(MenuId.AgentsControlMenu, { - command: { - id: 'workbench.action.chat.toggle', - title: localize('openChat', "Open Chat"), - }, - when: ContextKeyExpr.has(`config.${ChatConfiguration.AgentStatusEnabled}`), -}); - -/** - * Provides custom rendering for the agent status in the command center. - * Uses IActionViewItemService to render a custom AgentStatusWidget - * for the AgentsControlMenu submenu. - * Also adds a CSS class to the workbench when agent status is enabled. - */ -class AgentStatusRendering extends Disposable implements IWorkbenchContribution { - - static readonly ID = 'workbench.contrib.agentStatus.rendering'; - - constructor( - @IActionViewItemService actionViewItemService: IActionViewItemService, - @IInstantiationService instantiationService: IInstantiationService, - @IConfigurationService configurationService: IConfigurationService - ) { - super(); - - this._register(actionViewItemService.register(MenuId.CommandCenter, MenuId.AgentsControlMenu, (action, options) => { - if (!(action instanceof SubmenuItemAction)) { - return undefined; - } - return instantiationService.createInstance(AgentStatusWidget, action, options); - }, undefined)); - - // Add/remove CSS class on workbench based on setting - // Also force enable command center when agent status is enabled - const updateClass = () => { - const enabled = configurationService.getValue(ChatConfiguration.AgentStatusEnabled) === true; - mainWindow.document.body.classList.toggle('agent-status-enabled', enabled); - - // Force enable command center when agent status is enabled - if (enabled && configurationService.getValue(LayoutSettings.COMMAND_CENTER) !== true) { - configurationService.updateValue(LayoutSettings.COMMAND_CENTER, true); - } - }; - updateClass(); - this._register(configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(ChatConfiguration.AgentStatusEnabled)) { - updateClass(); - } - })); - } -} - -// Register the workbench contribution that provides custom rendering for the agent status -registerWorkbenchContribution2(AgentStatusRendering.ID, AgentStatusRendering, WorkbenchPhase.AfterRestored); //#endregion diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts index a72ab1cd2e6..1741e73eace 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessions.ts @@ -68,10 +68,12 @@ export enum AgentSessionsViewerPosition { } export interface IAgentSessionsControl { + + readonly element: HTMLElement | undefined; + refresh(): void; openFind(): void; reveal(sessionResource: URI): void; - setGridMarginOffset(offset: number): void; } export const agentSessionReadIndicatorForeground = registerColor( diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index 3ca1f18b287..a44fd400086 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -33,19 +33,17 @@ import { openSession } from './agentSessionsOpener.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js'; import { IMouseEvent } from '../../../../../base/browser/mouseEvent.js'; +import { IChatWidget } from '../chat.js'; export interface IAgentSessionsControlOptions extends IAgentSessionsSorterOptions { readonly overrideStyles: IStyleOverride; readonly filter: IAgentSessionsFilter; - readonly source: AgentSessionsControlSource; + readonly source: string; getHoverPosition(): HoverPosition; trackActiveEditorSession(): boolean; -} -export const enum AgentSessionsControlSource { - ChatViewPane = 'chatViewPane', - WelcomeView = 'welcomeView' + notifySessionOpened?(resource: URI, widget: IChatWidget): void; } type AgentSessionOpenedClassification = { @@ -57,12 +55,14 @@ type AgentSessionOpenedClassification = { type AgentSessionOpenedEvent = { providerType: string; - source: AgentSessionsControlSource; + source: string; }; export class AgentSessionsControl extends Disposable implements IAgentSessionsControl { private sessionsContainer: HTMLElement | undefined; + get element(): HTMLElement | undefined { return this.sessionsContainer; } + private sessionsList: WorkbenchCompressibleAsyncDataTree | undefined; private visible: boolean = true; @@ -213,7 +213,10 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo source: this.options.source }); - await this.instantiationService.invokeFunction(openSession, element, { ...e, expanded: this.options.source === AgentSessionsControlSource.WelcomeView }); + const widget = await this.instantiationService.invokeFunction(openSession, element, e); + if (widget) { + this.options.notifySessionOpened?.(element.resource, widget); + } } private async showContextMenu({ element, anchor, browserEvent }: ITreeContextMenuEvent): Promise { @@ -358,10 +361,4 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo this.sessionsList.setFocus([session]); this.sessionsList.setSelection([session]); } - - setGridMarginOffset(offset: number): void { - if (this.sessionsContainer) { - this.sessionsContainer.style.marginBottom = `-${offset}px`; - } - } } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts index 7220766b9df..47e10c26ce5 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsOpener.ts @@ -3,39 +3,65 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { IAgentSession, isLocalAgentSessionItem } from './agentSessionsModel.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { IChatEditorOptions } from '../widgetHosts/editor/chatEditor.js'; -import { ChatViewPaneTarget, IChatWidgetService } from '../chat.js'; +import { ChatViewPaneTarget, IChatWidget, IChatWidgetService } from '../chat.js'; import { ACTIVE_GROUP, SIDE_GROUP } from '../../../../services/editor/common/editorService.js'; import { IEditorOptions } from '../../../../../platform/editor/common/editor.js'; import { IChatSessionsService } from '../../common/chatSessionsService.js'; import { Schemas } from '../../../../../base/common/network.js'; -import { IAgentSessionProjectionService } from './agentSessionProjectionService.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { ChatConfiguration } from '../../common/constants.js'; -export async function openSession(accessor: ServicesAccessor, session: IAgentSession, openOptions?: { sideBySide?: boolean; editorOptions?: IEditorOptions; expanded?: boolean }): Promise { - const configurationService = accessor.get(IConfigurationService); - const projectionService = accessor.get(IAgentSessionProjectionService); +//#region Session Opener Registry - session.setRead(true); // mark as read when opened +export interface ISessionOpenerParticipant { + handleOpenSession(accessor: ServicesAccessor, session: IAgentSession, openOptions?: ISessionOpenOptions): Promise; +} - const agentSessionProjectionEnabled = configurationService.getValue(ChatConfiguration.AgentSessionProjectionEnabled) === true; - if (agentSessionProjectionEnabled) { - // Enter Agent Session Projection mode for the session - await projectionService.enterProjection(session); - } else { - // Fall back to opening in chat widget when Agent Session Projection is disabled - await openSessionInChatWidget(accessor, session, openOptions); +export interface ISessionOpenOptions { + readonly sideBySide?: boolean; + readonly editorOptions?: IEditorOptions; +} + +class SessionOpenerRegistry { + + private readonly participants = new Set(); + + registerParticipant(participant: ISessionOpenerParticipant): IDisposable { + this.participants.add(participant); + + return { + dispose: () => { + this.participants.delete(participant); + } + }; + } + + getParticipants(): readonly ISessionOpenerParticipant[] { + return Array.from(this.participants); } } -/** - * Opens a session in the traditional chat widget (side panel or editor). - * Use this when you explicitly want to open in the chat widget rather than agent session projection mode. - */ -export async function openSessionInChatWidget(accessor: ServicesAccessor, session: IAgentSession, openOptions?: { sideBySide?: boolean; editorOptions?: IEditorOptions; expanded?: boolean }): Promise { +export const sessionOpenerRegistry = new SessionOpenerRegistry(); + +//#endregion + +export async function openSession(accessor: ServicesAccessor, session: IAgentSession, openOptions?: ISessionOpenOptions): Promise { + + // First, give registered participants a chance to handle the session + for (const participant of sessionOpenerRegistry.getParticipants()) { + const handled = await participant.handleOpenSession(accessor, session, openOptions); + if (handled) { + return undefined; // Participant handled the session, skip default opening + } + } + + // Default session opening logic + return openSessionDefault(accessor, session, openOptions); +} + +async function openSessionDefault(accessor: ServicesAccessor, session: IAgentSession, openOptions?: ISessionOpenOptions): Promise { const chatSessionsService = accessor.get(IChatSessionsService); const chatWidgetService = accessor.get(IChatWidgetService); @@ -52,7 +78,6 @@ export async function openSessionInChatWidget(accessor: ServicesAccessor, sessio ...sessionOptions, ...openOptions?.editorOptions, revealIfOpened: true, // always try to reveal if already opened - expanded: openOptions?.expanded }; await chatSessionsService.activateChatSessionItemProvider(session.providerType); // ensure provider is activated before trying to open @@ -70,5 +95,5 @@ export async function openSessionInChatWidget(accessor: ServicesAccessor, sessio options = { ...options, revealIfOpened: true }; } - await chatWidgetService.openSession(session.resource, target, options); + return chatWidgetService.openSession(session.resource, target, options); } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjection.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjection.ts new file mode 100644 index 00000000000..1984aa24605 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjection.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from '../../../../../../nls.js'; +import { RawContextKey } from '../../../../../../platform/contextkey/common/contextkey.js'; + +export const inAgentSessionProjection = new RawContextKey('chatInAgentSessionProjection', false, { type: 'boolean', description: localize('chatInAgentSessionProjection', "True when the workbench is in agent session projection mode for reviewing an agent session.") }); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionActions.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionActions.ts similarity index 74% rename from src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionActions.ts rename to src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionActions.ts index 7571d0f8e50..1e017fc3d96 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionActions.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionActions.ts @@ -3,20 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize, localize2 } from '../../../../../nls.js'; -import { Action2 } from '../../../../../platform/actions/common/actions.js'; -import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; -import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { KeyCode } from '../../../../../base/common/keyCodes.js'; -import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; -import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; -import { ChatConfiguration } from '../../common/constants.js'; +import { localize, localize2 } from '../../../../../../nls.js'; +import { Action2 } from '../../../../../../platform/actions/common/actions.js'; +import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { KeybindingWeight } from '../../../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { KeyCode } from '../../../../../../base/common/keyCodes.js'; +import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; import { IAgentSessionProjectionService } from './agentSessionProjectionService.js'; -import { IAgentSession, isMarshalledAgentSessionContext, IMarshalledAgentSessionContext } from './agentSessionsModel.js'; -import { IAgentSessionsService } from './agentSessionsService.js'; -import { CHAT_CATEGORY } from '../actions/chatActions.js'; -import { ToggleTitleBarConfigAction } from '../../../../browser/parts/titlebar/titlebarActions.js'; -import { IsCompactTitleBarContext } from '../../../../common/contextkeys.js'; +import { IAgentSession, isMarshalledAgentSessionContext, IMarshalledAgentSessionContext } from '../agentSessionsModel.js'; +import { IAgentSessionsService } from '../agentSessionsService.js'; +import { CHAT_CATEGORY } from '../../actions/chatActions.js'; +import { ToggleTitleBarConfigAction } from '../../../../../browser/parts/titlebar/titlebarActions.js'; +import { IsCompactTitleBarContext } from '../../../../../common/contextkeys.js'; +import { inAgentSessionProjection } from './agentSessionProjection.js'; +import { ChatConfiguration } from '../../../common/constants.js'; //#region Enter Agent Session Projection @@ -32,7 +33,7 @@ export class EnterAgentSessionProjectionAction extends Action2 { precondition: ContextKeyExpr.and( ChatContextKeys.enabled, ContextKeyExpr.has(`config.${ChatConfiguration.AgentSessionProjectionEnabled}`), - ChatContextKeys.inAgentSessionProjection.negate() + inAgentSessionProjection.negate() ), }); } @@ -71,12 +72,12 @@ export class ExitAgentSessionProjectionAction extends Action2 { f1: true, precondition: ContextKeyExpr.and( ChatContextKeys.enabled, - ChatContextKeys.inAgentSessionProjection + inAgentSessionProjection ), keybinding: { weight: KeybindingWeight.WorkbenchContrib, primary: KeyCode.Escape, - when: ChatContextKeys.inAgentSessionProjection, + when: inAgentSessionProjection, }, }); } diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionService.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts similarity index 81% rename from src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionService.ts rename to src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts index 8521dd2ecd7..67e8db416a0 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionProjectionService.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionProjectionService.ts @@ -3,28 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import './media/agentSessionProjection.css'; - -import { Emitter, Event } from '../../../../../base/common/event.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { localize } from '../../../../../nls.js'; -import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; -import { ILogService } from '../../../../../platform/log/common/log.js'; -import { IEditorGroupsService, IEditorWorkingSet } from '../../../../services/editor/common/editorGroupsService.js'; -import { IEditorService } from '../../../../services/editor/common/editorService.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; -import { IAgentSession } from './agentSessionsModel.js'; -import { ChatViewPaneTarget, IChatWidgetService } from '../chat.js'; -import { AgentSessionProviders } from './agentSessions.js'; -import { IChatSessionsService } from '../../common/chatSessionsService.js'; -import { ChatConfiguration } from '../../common/constants.js'; -import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; -import { ACTION_ID_NEW_CHAT } from '../actions/chatActions.js'; -import { IChatEditingService, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js'; -import { IAgentStatusService } from './agentStatusService.js'; +import './media/agentsessionprojection.css'; +import { Emitter, Event } from '../../../../../../base/common/event.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { localize } from '../../../../../../nls.js'; +import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { createDecorator } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { IEditorGroupsService, IEditorWorkingSet } from '../../../../../services/editor/common/editorGroupsService.js'; +import { IEditorService } from '../../../../../services/editor/common/editorService.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; +import { IAgentSession } from '../agentSessionsModel.js'; +import { ChatViewPaneTarget, IChatWidgetService } from '../../chat.js'; +import { AgentSessionProviders } from '../agentSessions.js'; +import { IChatSessionsService } from '../../../common/chatSessionsService.js'; +import { IWorkbenchLayoutService } from '../../../../../services/layout/browser/layoutService.js'; +import { ACTION_ID_NEW_CHAT } from '../../actions/chatActions.js'; +import { IChatEditingService, ModifiedFileEntryState } from '../../../common/editing/chatEditingService.js'; +import { IAgentTitleBarStatusService } from './agentTitleBarStatusService.js'; +import { ISessionOpenerParticipant, ISessionOpenOptions, sessionOpenerRegistry } from '../agentSessionsOpener.js'; +import { ServicesAccessor } from '../../../../../../editor/browser/editorExtensions.js'; +import { inAgentSessionProjection } from './agentSessionProjection.js'; +import { ChatConfiguration } from '../../../common/constants.js'; //#region Configuration @@ -113,14 +114,34 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @ICommandService private readonly commandService: ICommandService, @IChatEditingService private readonly chatEditingService: IChatEditingService, - @IAgentStatusService private readonly agentStatusService: IAgentStatusService, + @IAgentTitleBarStatusService private readonly agentTitleBarStatusService: IAgentTitleBarStatusService, ) { super(); - this._inProjectionModeContextKey = ChatContextKeys.inAgentSessionProjection.bindTo(contextKeyService); + this._inProjectionModeContextKey = inAgentSessionProjection.bindTo(contextKeyService); // Listen for editor close events to exit projection mode when all editors are closed this._register(this.editorService.onDidCloseEditor(() => this._checkForEmptyEditors())); + + // Register as a session opener participant to enter projection mode when sessions are opened + this._register(sessionOpenerRegistry.registerParticipant(this._createSessionOpenerParticipant())); + } + + private _createSessionOpenerParticipant(): ISessionOpenerParticipant { + return { + handleOpenSession: async (_accessor: ServicesAccessor, session: IAgentSession, _openOptions?: ISessionOpenOptions): Promise => { + // Only handle if projection mode is enabled + if (!this._isEnabled()) { + return false; + } + + // Enter projection mode for the session + await this.enterProjection(session); + + // Return true to indicate we handled the session (projection mode opens the chat itself) + return true; + } + }; } private _isEnabled(): boolean { @@ -260,7 +281,7 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS this.layoutService.mainContainer.classList.add('agent-session-projection-active'); // Update the agent status to show session mode - this.agentStatusService.enterSessionMode(session.resource.toString(), session.label); + this.agentTitleBarStatusService.enterSessionMode(session.resource.toString(), session.label); if (!wasActive) { this._onDidChangeProjectionMode.fire(true); @@ -316,7 +337,7 @@ export class AgentSessionProjectionService extends Disposable implements IAgentS this.layoutService.mainContainer.classList.remove('agent-session-projection-active'); // Update the agent status to exit session mode - this.agentStatusService.exitSessionMode(); + this.agentTitleBarStatusService.exitSessionMode(); this._onDidChangeProjectionMode.fire(false); this._onDidChangeActiveSession.fire(undefined); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionsExperiments.contribution.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionsExperiments.contribution.ts new file mode 100644 index 00000000000..f510c88aaca --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionsExperiments.contribution.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerSingleton, InstantiationType } from '../../../../../../platform/instantiation/common/extensions.js'; +import { MenuId, MenuRegistry, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; +import { IAgentSessionProjectionService, AgentSessionProjectionService } from './agentSessionProjectionService.js'; +import { EnterAgentSessionProjectionAction, ExitAgentSessionProjectionAction, ToggleAgentStatusAction, ToggleAgentSessionProjectionAction } from './agentSessionProjectionActions.js'; +import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../../common/contributions.js'; +import { AgentTitleBarStatusRendering } from './agentTitleBarStatusWidget.js'; +import { AgentTitleBarStatusService, IAgentTitleBarStatusService } from './agentTitleBarStatusService.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { localize } from '../../../../../../nls.js'; +import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { ChatConfiguration } from '../../../common/constants.js'; + +// #region Agent Session Projection & Status + +registerAction2(EnterAgentSessionProjectionAction); +registerAction2(ExitAgentSessionProjectionAction); +registerAction2(ToggleAgentStatusAction); +registerAction2(ToggleAgentSessionProjectionAction); + +registerSingleton(IAgentSessionProjectionService, AgentSessionProjectionService, InstantiationType.Delayed); +registerSingleton(IAgentTitleBarStatusService, AgentTitleBarStatusService, InstantiationType.Delayed); + +registerWorkbenchContribution2(AgentTitleBarStatusRendering.ID, AgentTitleBarStatusRendering, WorkbenchPhase.AfterRestored); + +// Register Agent Status as a menu item in the command center (alongside the search box, not replacing it) +MenuRegistry.appendMenuItem(MenuId.CommandCenter, { + submenu: MenuId.AgentsTitleBarControlMenu, + title: localize('agentsControl', "Agents"), + icon: Codicon.chatSparkle, + when: ContextKeyExpr.has(`config.${ChatConfiguration.AgentStatusEnabled}`), + order: 10002 // to the right of the chat button +}); + +// Register a placeholder action to the submenu so it appears (required for submenus) +MenuRegistry.appendMenuItem(MenuId.AgentsTitleBarControlMenu, { + command: { + id: 'workbench.action.chat.toggle', + title: localize('openChat', "Open Chat"), + }, + when: ContextKeyExpr.has(`config.${ChatConfiguration.AgentStatusEnabled}`), +}); + +//#endregion diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusService.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusService.ts similarity index 86% rename from src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusService.ts rename to src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusService.ts index a6607e468f5..0c8389b4060 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusService.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusService.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter, Event } from '../../../../../base/common/event.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import { Emitter, Event } from '../../../../../../base/common/event.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { createDecorator } from '../../../../../../platform/instantiation/common/instantiation.js'; //#region Agent Status Mode @@ -25,7 +25,7 @@ export interface IAgentStatusSessionInfo { //#region Agent Status Service Interface -export interface IAgentStatusService { +export interface IAgentTitleBarStatusService { readonly _serviceBrand: undefined; /** @@ -66,13 +66,13 @@ export interface IAgentStatusService { updateSessionTitle(title: string): void; } -export const IAgentStatusService = createDecorator('agentStatusService'); +export const IAgentTitleBarStatusService = createDecorator('agentTitleBarStatusService'); //#endregion //#region Agent Status Service Implementation -export class AgentStatusService extends Disposable implements IAgentStatusService { +export class AgentTitleBarStatusService extends Disposable implements IAgentTitleBarStatusService { declare readonly _serviceBrand: undefined; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts similarity index 84% rename from src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts rename to src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts index 6bfea0a5b27..bb4bdaf6386 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentStatusWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts @@ -3,39 +3,44 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import './media/agentStatusWidget.css'; - -import { $, addDisposableListener, EventType, reset } from '../../../../../base/browser/dom.js'; -import { renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { Codicon } from '../../../../../base/common/codicons.js'; -import { localize } from '../../../../../nls.js'; -import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; -import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; -import { AgentStatusMode, IAgentStatusService } from './agentStatusService.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; +import './media/agenttitlebarstatuswidget.css'; +import { $, addDisposableListener, EventType, reset } from '../../../../../../base/browser/dom.js'; +import { renderIcon } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { localize } from '../../../../../../nls.js'; +import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; +import { getDefaultHoverDelegate } from '../../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; +import { AgentStatusMode, IAgentTitleBarStatusService } from './agentTitleBarStatusService.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; +import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; import { ExitAgentSessionProjectionAction } from './agentSessionProjectionActions.js'; -import { IAgentSessionsService } from './agentSessionsService.js'; -import { AgentSessionStatus, IAgentSession, isSessionInProgressStatus } from './agentSessionsModel.js'; -import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; -import { IAction, SubmenuAction } from '../../../../../base/common/actions.js'; -import { ILabelService } from '../../../../../platform/label/common/label.js'; -import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { IBrowserWorkbenchEnvironmentService } from '../../../../services/environment/browser/environmentService.js'; -import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; -import { IEditorService } from '../../../../services/editor/common/editorService.js'; -import { Verbosity } from '../../../../common/editor.js'; -import { Schemas } from '../../../../../base/common/network.js'; -import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js'; -import { openSession } from './agentSessionsOpener.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; -import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { HiddenItemStrategy, WorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; -import { createActionViewItem } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; -import { FocusAgentSessionsAction } from './agentSessionsActions.js'; +import { IAgentSessionsService } from '../agentSessionsService.js'; +import { AgentSessionStatus, IAgentSession, isSessionInProgressStatus } from '../agentSessionsModel.js'; +import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { IAction, SubmenuAction } from '../../../../../../base/common/actions.js'; +import { ILabelService } from '../../../../../../platform/label/common/label.js'; +import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; +import { IBrowserWorkbenchEnvironmentService } from '../../../../../services/environment/browser/environmentService.js'; +import { IEditorGroupsService } from '../../../../../services/editor/common/editorGroupsService.js'; +import { IEditorService } from '../../../../../services/editor/common/editorService.js'; +import { Verbosity } from '../../../../../common/editor.js'; +import { Schemas } from '../../../../../../base/common/network.js'; +import { renderAsPlaintext } from '../../../../../../base/browser/markdownRenderer.js'; +import { openSession } from '../agentSessionsOpener.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IMenuService, MenuId, SubmenuItemAction } from '../../../../../../platform/actions/common/actions.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { HiddenItemStrategy, WorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js'; +import { createActionViewItem } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; +import { FocusAgentSessionsAction } from '../agentSessionsActions.js'; +import { IWorkbenchContribution } from '../../../../../common/contributions.js'; +import { IActionViewItemService } from '../../../../../../platform/actions/browser/actionViewItemService.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { mainWindow } from '../../../../../../base/browser/window.js'; +import { LayoutSettings } from '../../../../../services/layout/browser/layoutService.js'; +import { ChatConfiguration } from '../../../common/constants.js'; // Action triggered when clicking the main pill - change this to modify the primary action const ACTION_ID = 'workbench.action.quickchat.toggle'; @@ -53,7 +58,7 @@ const TITLE_DIRTY = '\u25cf '; * * The command center search box and navigation controls remain visible alongside this control. */ -export class AgentStatusWidget extends BaseActionViewItem { +export class AgentTitleBarStatusWidget extends BaseActionViewItem { private static readonly _quickOpenCommandId = 'workbench.action.quickOpenWithModes'; @@ -73,7 +78,7 @@ export class AgentStatusWidget extends BaseActionViewItem { action: IAction, options: IBaseActionViewItemOptions | undefined, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IAgentStatusService private readonly agentStatusService: IAgentStatusService, + @IAgentTitleBarStatusService private readonly agentTitleBarStatusService: IAgentTitleBarStatusService, @IHoverService private readonly hoverService: IHoverService, @ICommandService private readonly commandService: ICommandService, @IKeybindingService private readonly keybindingService: IKeybindingService, @@ -93,11 +98,11 @@ export class AgentStatusWidget extends BaseActionViewItem { this._commandCenterMenu = this._register(this.menuService.createMenu(MenuId.CommandCenterCenter, this.contextKeyService)); // Re-render when control mode or session info changes - this._register(this.agentStatusService.onDidChangeMode(() => { + this._register(this.agentTitleBarStatusService.onDidChangeMode(() => { this._render(); })); - this._register(this.agentStatusService.onDidChangeSessionInfo(() => { + this._register(this.agentTitleBarStatusService.onDidChangeSessionInfo(() => { this._render(); })); @@ -140,8 +145,8 @@ export class AgentStatusWidget extends BaseActionViewItem { } // Compute current render state to avoid unnecessary DOM rebuilds - const mode = this.agentStatusService.mode; - const sessionInfo = this.agentStatusService.sessionInfo; + const mode = this.agentTitleBarStatusService.mode; + const sessionInfo = this.agentTitleBarStatusService.sessionInfo; const { activeSessions, unreadSessions, attentionNeededSessions } = this._getSessionStats(); // Get attention session info for state computation @@ -184,7 +189,7 @@ export class AgentStatusWidget extends BaseActionViewItem { // Clear previous disposables for dynamic content this._dynamicDisposables.clear(); - if (this.agentStatusService.mode === AgentStatusMode.Session) { + if (this.agentTitleBarStatusService.mode === AgentStatusMode.Session) { // Agent Session Projection mode - show session title + close button this._renderSessionMode(this._dynamicDisposables); } else { @@ -352,7 +357,7 @@ export class AgentStatusWidget extends BaseActionViewItem { // Session title (center) const titleLabel = $('span.agent-status-title'); - const sessionInfo = this.agentStatusService.sessionInfo; + const sessionInfo = this.agentTitleBarStatusService.sessionInfo; titleLabel.textContent = sessionInfo?.title ?? localize('agentSessionProjection', "Agent Session Projection"); pill.appendChild(titleLabel); @@ -362,7 +367,7 @@ export class AgentStatusWidget extends BaseActionViewItem { // Setup pill hover const hoverDelegate = getDefaultHoverDelegate('mouse'); disposables.add(this.hoverService.setupManagedHover(hoverDelegate, pill, () => { - const sessionInfo = this.agentStatusService.sessionInfo; + const sessionInfo = this.agentTitleBarStatusService.sessionInfo; return sessionInfo ? localize('agentSessionProjectionTooltip', "Agent Session Projection: {0}", sessionInfo.title) : localize('agentSessionProjection', "Agent Session Projection"); })); @@ -389,7 +394,7 @@ export class AgentStatusWidget extends BaseActionViewItem { for (const [, actions] of this._commandCenterMenu.getActions({ shouldForwardArgs: true })) { for (const action of actions) { // Filter out the quick open action - we provide our own search UI - if (action.id === AgentStatusWidget._quickOpenCommandId) { + if (action.id === AgentTitleBarStatusWidget._quickOpenCommandId) { continue; } // For submenus (like debug toolbar), add the submenu actions @@ -824,3 +829,47 @@ export class AgentStatusWidget extends BaseActionViewItem { // #endregion } + +/** + * Provides custom rendering for the agent status in the command center. + * Uses IActionViewItemService to render a custom AgentStatusWidget + * for the AgentsControlMenu submenu. + * Also adds a CSS class to the workbench when agent status is enabled. + */ +export class AgentTitleBarStatusRendering extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.agentStatus.rendering'; + + constructor( + @IActionViewItemService actionViewItemService: IActionViewItemService, + @IInstantiationService instantiationService: IInstantiationService, + @IConfigurationService configurationService: IConfigurationService + ) { + super(); + + this._register(actionViewItemService.register(MenuId.CommandCenter, MenuId.AgentsTitleBarControlMenu, (action, options) => { + if (!(action instanceof SubmenuItemAction)) { + return undefined; + } + return instantiationService.createInstance(AgentTitleBarStatusWidget, action, options); + }, undefined)); + + // Add/remove CSS class on workbench based on setting + // Also force enable command center when agent status is enabled + const updateClass = () => { + const enabled = configurationService.getValue(ChatConfiguration.AgentStatusEnabled) === true; + mainWindow.document.body.classList.toggle('agent-status-enabled', enabled); + + // Force enable command center when agent status is enabled + if (enabled && configurationService.getValue(LayoutSettings.COMMAND_CENTER) !== true) { + configurationService.updateValue(LayoutSettings.COMMAND_CENTER, true); + } + }; + updateClass(); + this._register(configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(ChatConfiguration.AgentStatusEnabled)) { + updateClass(); + } + })); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionProjection.css b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agentsessionprojection.css similarity index 94% rename from src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionProjection.css rename to src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agentsessionprojection.css index d6d3b3c3694..7f64094c2b4 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentSessionProjection.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agentsessionprojection.css @@ -3,10 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* ======================================== -Agent Session Projection Mode - Tab and Editor styling -======================================== */ - /* Style all tabs with the same background as the agent status */ .monaco-workbench.agent-session-projection-active .part.editor > .content .editor-group-container > .title .tabs-container > .tab { background-color: color-mix(in srgb, var(--vscode-progressBar-background) 15%, transparent) !important; diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css similarity index 98% rename from src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css rename to src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css index e1d663108da..e4af6e88de4 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentStatusWidget.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css @@ -3,10 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* ======================================== -Agent Status Widget - Titlebar control -======================================== */ - /* Hide command center search box when agent status enabled */ .agent-status-enabled .command-center .action-item.command-center-center { display: none !important; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts index f8b867d9a49..2deb9859f16 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts @@ -19,7 +19,6 @@ import { ChatViewId, ChatViewPaneTarget, IChatWidget, IChatWidgetService, IQuick import { ChatEditor, IChatEditorOptions } from '../widgetHosts/editor/chatEditor.js'; import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js'; import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js'; -import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; export class ChatWidgetService extends Disposable implements IChatWidgetService { @@ -41,7 +40,6 @@ export class ChatWidgetService extends Disposable implements IChatWidgetService @ILayoutService private readonly layoutService: ILayoutService, @IEditorService private readonly editorService: IEditorService, @IChatService private readonly chatService: IChatService, - @IWorkbenchLayoutService private readonly workbenchLayoutService: IWorkbenchLayoutService, ) { super(); } @@ -118,9 +116,6 @@ export class ChatWidgetService extends Disposable implements IChatWidgetService if (!options?.preserveFocus) { chatView.focusInput(); } - if (options?.expanded) { - this.workbenchLayoutService.setAuxiliaryBarMaximized(true); - } } return chatView?.widget; } diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditor.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditor.ts index 3f8e0e9d7a9..fffb5a27aa8 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditor.ts @@ -43,7 +43,6 @@ export interface IChatEditorOptions extends IEditorOptions { preferred?: string; fallback?: string; }; - expanded?: boolean; } export class ChatEditor extends EditorPane { diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 52bfac795e0..7fd857ffb22 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -47,7 +47,7 @@ import { IChatModelReference, IChatService } from '../../../common/chatService/c import { IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js'; import { LocalChatSessionUri, getChatSessionType } from '../../../common/model/chatUri.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../../common/constants.js'; -import { AgentSessionsControl, AgentSessionsControlSource } from '../../agentSessions/agentSessionsControl.js'; +import { AgentSessionsControl } from '../../agentSessions/agentSessionsControl.js'; import { AgentSessionsListDelegate } from '../../agentSessions/agentSessionsViewer.js'; import { ACTION_ID_NEW_CHAT } from '../../actions/chatActions.js'; import { ChatWidget } from '../../widget/chatWidget.js'; @@ -394,7 +394,7 @@ 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, { - source: AgentSessionsControlSource.ChatViewPane, + source: 'chatViewPane', filter: sessionsFilter, overrideStyles: this.getLocationBasedColors().listOverrideStyles, getHoverPosition: () => { diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index 7da745d17d2..ed0de643267 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -108,10 +108,6 @@ export namespace ChatContextKeys { export const hasAgentSessionChanges = new RawContextKey('agentSessionHasChanges', false, { type: 'boolean', description: localize('agentSessionHasChanges', "True when the current agent session item has changes.") }); export const isKatexMathElement = new RawContextKey('chatIsKatexMathElement', false, { type: 'boolean', description: localize('chatIsKatexMathElement', "True when focusing a KaTeX math element.") }); - - export const inAgentSessionProjection = new RawContextKey('chatInAgentSessionProjection', false, { type: 'boolean', description: localize('chatInAgentSessionProjection', "True when the workbench is in agent session projection mode for reviewing an agent session.") }); - - export const agentStatusHasNotifications = new RawContextKey('agentStatusHasNotifications', false, { type: 'boolean', description: localize('agentStatusHasNotifications', "True when the agent status widget has unread or in-progress sessions.") }); } export namespace ChatContextKeyExprs { diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts index ed1689fb2e2..e437e7144ef 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts @@ -40,7 +40,7 @@ import { AgentSessionsWelcomeEditorOptions, AgentSessionsWelcomeInput } from './ import { IChatService } from '../../chat/common/chatService/chatService.js'; import { IChatModel } from '../../chat/common/model/chatModel.js'; import { ISessionTypePickerDelegate } from '../../chat/browser/chat.js'; -import { AgentSessionsControl, AgentSessionsControlSource, IAgentSessionsControlOptions } from '../../chat/browser/agentSessions/agentSessionsControl.js'; +import { AgentSessionsControl, IAgentSessionsControlOptions } from '../../chat/browser/agentSessions/agentSessionsControl.js'; import { IAgentSessionsFilter } from '../../chat/browser/agentSessions/agentSessionsViewer.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { IResolvedWalkthrough, IWalkthroughsService } from '../../welcomeGettingStarted/browser/gettingStartedService.js'; @@ -280,7 +280,8 @@ export class AgentSessionsWelcomePage extends EditorPane { filter, getHoverPosition: () => HoverPosition.BELOW, trackActiveEditorSession: () => false, - source: AgentSessionsControlSource.WelcomeView, + source: 'welcomeView', + notifySessionOpened: () => this.layoutService.setAuxiliaryBarMaximized(true) // TODO@osortega what if the session did not open in the 2nd sidebar? }; this.sessionsControl = this.sessionsControlDisposables.add(this.instantiationService.createInstance( @@ -433,7 +434,7 @@ export class AgentSessionsWelcomePage extends EditorPane { // Set margin offset for 2-column layout: actual height - visual height // Visual height = ceil(n/2) * 52, so offset = floor(n/2) * 52 const marginOffset = Math.floor(visibleSessions / 2) * 52; - this.sessionsControl.setGridMarginOffset(marginOffset); + this.sessionsControl.element!.style.marginBottom = `-${marginOffset}px`; } override focus(): void {