From 917bcb915ef6918d98784de857f19bfda38a7d72 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 18 Sep 2026 17:46:34 +0200 Subject: [PATCH 1/4] sessions: move Customizations next to Automations Replace the separate Customizations shortcuts pane with a single sidebar entry and place the Sessions header directly below the navigation shortcuts. Preserve the existing management editor, header actions, mobile behavior, accessibility, and session-row hover treatment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/sessions/browser/menus.ts | 1 - .../browser/sessionsChatAccessibilityHelp.ts | 2 +- .../browser/hostFilterActionViewItem.ts | 16 +- .../browser/media/hostFilter.css | 2 +- .../browser/agentHostShortcutsWidget.ts | 6 +- .../browser/aiCustomizationShortcutsWidget.ts | 258 ----------- .../browser/customizations.contribution.ts | 105 +++++ .../browser/customizationsConstants.ts | 6 + .../customizationsToolbar.contribution.ts | 417 ------------------ .../browser/media/agentHostToolbar.css | 6 +- .../browser/media/customizationsToolbar.css | 192 -------- .../sessions/browser/media/sessionsList.css | 32 +- .../browser/media/sessionsViewPane.css | 21 +- .../sessions/browser/views/sessionsList.ts | 151 ++++++- .../sessions/browser/views/sessionsView.ts | 102 +---- .../browser/views/sessionsViewActions.ts | 5 +- .../aiCustomizationShortcutsWidget.fixture.ts | 266 ----------- .../test/browser/sessionsList.test.ts | 84 ++++ .../test/browser/sessionsRename.test.ts | 4 +- .../test/browser/sessionsViewPane.test.ts | 86 +--- src/vs/sessions/sessions.common.main.ts | 2 +- 21 files changed, 406 insertions(+), 1358 deletions(-) delete mode 100644 src/vs/sessions/contrib/sessions/browser/aiCustomizationShortcutsWidget.ts create mode 100644 src/vs/sessions/contrib/sessions/browser/customizations.contribution.ts create mode 100644 src/vs/sessions/contrib/sessions/browser/customizationsConstants.ts delete mode 100644 src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts delete mode 100644 src/vs/sessions/contrib/sessions/browser/media/customizationsToolbar.css delete mode 100644 src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts diff --git a/src/vs/sessions/browser/menus.ts b/src/vs/sessions/browser/menus.ts index b6d1a43fd5b1a7..877e891e21a671 100644 --- a/src/vs/sessions/browser/menus.ts +++ b/src/vs/sessions/browser/menus.ts @@ -30,7 +30,6 @@ export const Menus = { SessionsViewExternalFilter: new MenuId('SessionsViewExternalFilter'), AuxiliaryBarTitle: new MenuId('SessionsAuxiliaryBarTitle'), SidebarFooter: new MenuId('SessionsSidebarFooter'), - SidebarCustomizations: new MenuId('SessionsSidebarCustomizations'), SidebarAgentHost: new MenuId('SessionsSidebarAgentHost'), AccountMenu: new MenuId('SessionsAccountMenu'), GoMenu: new MenuId('SessionsGoMenu'), diff --git a/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts b/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts index cefd6b59a13c5c..273e8b6eefeff6 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts @@ -122,7 +122,7 @@ export class SessionsChatAccessibilityHelp implements IAccessibleViewImplementat content.push(localize('sessionsChat.filesView', "Focus the Files Explorer view{0}.", '')); content.push(localize('sessionsChat.sessionsView', "Focus the Chat Sessions view{0}.", '')); if (!isPhoneLayout(accessor.get(IWorkbenchLayoutService))) { - content.push(localize('sessionsChat.customizations', "Focus the Chat Customizations section at the bottom of the left sidebar{0}.", ``)); + content.push(localize('sessionsChat.customizations', "Focus the Customizations entry next to Automations in the left sidebar{0}.", ``)); } content.push(localize('sessionsChat.toggleSidePanel', "Toggle the side panel (the editor area together with the auxiliary bar) open or closed{0}.", '')); diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/hostFilterActionViewItem.ts b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/hostFilterActionViewItem.ts index 95935fdebb28c1..7997fa1b88ba4b 100644 --- a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/hostFilterActionViewItem.ts +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/hostFilterActionViewItem.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import './media/hostFilter.css'; +import '../../../../browser/media/sidebarActionButton.css'; import * as dom from '../../../../../base/browser/dom.js'; import { Gesture, EventType as TouchEventType } from '../../../../../base/browser/touch.js'; import { renderIcon, renderLabelWithIcons } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; @@ -138,15 +139,7 @@ export class HostFilterActionViewItem extends BaseActionViewItem { this._renderDiagnosticsButton(this.element); } - /** - * Sidebar appearance — full-width row matching the Customizations links - * (`CustomizationLinkViewItem`). Same Monaco `Button` shell, same - * `.sidebar-action-button` styling, same `supportIcons` label rendering. - * The trailing connect indicator is rendered alongside the picker - * button as a sibling control, so the row visually mirrors the - * Customizations rows in the toolbar above without making the - * indicator part of the picker label. - */ + /** Renders the full-width sidebar variant with a separate connection indicator. */ private _renderSidebar(): void { if (!this.element) { return; @@ -154,11 +147,10 @@ export class HostFilterActionViewItem extends BaseActionViewItem { this.element.classList.add('sidebar-action'); - // Picker button — same shell as `CustomizationLinkViewItem`. We - // drive the button content manually (rather than via `Button.label`) + // Drive the button content manually (rather than via `Button.label`) // so the host name span can `flex: 1` and push the chevron all // the way to the trailing edge. - const buttonContainer = dom.append(this.element, dom.$('.customization-link-button-container')); + const buttonContainer = dom.append(this.element, dom.$('.agent-host-filter-button-container')); this._sidebarButton = this._register(new Button(buttonContainer, { ...defaultButtonStyles, secondary: true, diff --git a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/media/hostFilter.css b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/media/hostFilter.css index ea1f14d67bfea3..78818d35115363 100644 --- a/src/vs/sessions/contrib/providers/remoteAgentHost/browser/media/hostFilter.css +++ b/src/vs/sessions/contrib/providers/remoteAgentHost/browser/media/hostFilter.css @@ -36,7 +36,7 @@ gap: 4px; } -.agent-host-filter-combo.sidebar .customization-link-button-container { +.agent-host-filter-combo.sidebar .agent-host-filter-button-container { flex: 1 1 auto; min-width: 0; } diff --git a/src/vs/sessions/contrib/sessions/browser/agentHostShortcutsWidget.ts b/src/vs/sessions/contrib/sessions/browser/agentHostShortcutsWidget.ts index 381aca667e3c19..384d2b6bde6f27 100644 --- a/src/vs/sessions/contrib/sessions/browser/agentHostShortcutsWidget.ts +++ b/src/vs/sessions/contrib/sessions/browser/agentHostShortcutsWidget.ts @@ -19,7 +19,7 @@ export interface IAgentHostShortcutsWidgetOptions { /** * Sidebar toolbar that hosts the agent host picker (with embedded * connect/disconnect indicator) on web desktop. Always expanded — there is - * no collapse affordance, unlike `AICustomizationShortcutsWidget`. + * no collapse affordance. * * Mounted only when `isWeb && !isPhoneLayout` (electron desktop has no host * picker today, and phone layout uses the mobile titlebar pill instead). @@ -37,9 +37,7 @@ export class AgentHostShortcutsWidget extends Disposable { } private _render(parent: HTMLElement, options: IAgentHostShortcutsWidgetOptions | undefined): void { - // Separates the picker from the customizations above it, mirroring the - // split view's separator between sessions and customizations — but - // static, since this section is not resizable. + // The picker is static rather than a resizable sidebar section. DOM.append(parent, $('.agent-host-toolbar-separator')); const container = DOM.append(parent, $('.agent-host-toolbar')); diff --git a/src/vs/sessions/contrib/sessions/browser/aiCustomizationShortcutsWidget.ts b/src/vs/sessions/contrib/sessions/browser/aiCustomizationShortcutsWidget.ts deleted file mode 100644 index 51d7a70c26f523..00000000000000 --- a/src/vs/sessions/contrib/sessions/browser/aiCustomizationShortcutsWidget.ts +++ /dev/null @@ -1,258 +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 '../../../browser/media/sidebarActionButton.css'; -import './media/customizationsToolbar.css'; -import * as DOM from '../../../../base/browser/dom.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { Emitter } from '../../../../base/common/event.js'; -import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { autorun, derived } from '../../../../base/common/observable.js'; -import { ScrollbarVisibility } from '../../../../base/common/scrollable.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { localize } from '../../../../nls.js'; -import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js'; -import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js'; -import { IAICustomizationItemsModel } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationItemsModel.js'; -import { ICustomizationHarnessService } from '../../../../workbench/contrib/chat/common/customizationHarnessService.js'; -import { CUSTOMIZATION_ITEMS } from './customizationsToolbar.contribution.js'; -import { Menus } from '../../../browser/menus.js'; -const $ = DOM.$; -const CUSTOMIZATIONS_VERTICAL_PADDING = 6; -const CUSTOMIZATIONS_COLLAPSED_STORAGE_KEY = 'agentSessions.customizationsShortcuts.collapsed'; - -export interface IAICustomizationShortcutsWidgetOptions { - readonly onDidChangeLayout?: () => void; -} - -export class AICustomizationShortcutsWidget extends Disposable { - - private _renderDisposables = this._register(new DisposableStore()); - private _wrapper: HTMLElement | undefined; - private _options: IAICustomizationShortcutsWidgetOptions | undefined; - private _scrollableElement: DomScrollableElement | undefined; - private _toolbar: MenuWorkbenchToolBar | undefined; - private _headerElement: HTMLElement | undefined; - private _headerTotalCountElement: HTMLElement | undefined; - private _chevronElement: HTMLElement | undefined; - private _toolbarContentElement: HTMLElement | undefined; - private _scrollableDomNode: HTMLElement | undefined; - private _rootVerticalPadding = 0; - private _headerTotalCount = 0; - private _collapsed = false; - - private readonly _onDidChangeHeight = this._register(new Emitter()); - readonly onDidChangeHeight = this._onDidChangeHeight.event; - - private readonly _onDidToggleCollapsed = this._register(new Emitter()); - readonly onDidToggleCollapsed = this._onDidToggleCollapsed.event; - - get collapsed(): boolean { - return this._collapsed; - } - - get collapsedHeight(): number { - const headerHeight = this._headerElement?.offsetHeight ?? 30; - return this._rootVerticalPadding + headerHeight; - } - - constructor( - container: HTMLElement, - options: IAICustomizationShortcutsWidgetOptions | undefined, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IMcpService private readonly mcpService: IMcpService, - @IAICustomizationItemsModel private readonly itemsModel: IAICustomizationItemsModel, - @ICustomizationHarnessService private readonly harnessService: ICustomizationHarnessService, - @IStorageService private readonly storageService: IStorageService, - ) { - super(); - - this._collapsed = this.storageService.getBoolean(CUSTOMIZATIONS_COLLAPSED_STORAGE_KEY, StorageScope.PROFILE, false); - - // Stable wrapper appended once to the parent. Re-renders replace the - // wrapper's children only, so the widget keeps its position relative - // to sibling parts (e.g. the agent-host-toolbar below it). Without - // this, removing+re-appending the rendered root would move it to the - // end of the parent on every re-render, stacking adjacent border-tops. - this._wrapper = DOM.append(container, $('.ai-customization-shortcuts-widget')); - this._options = options; - this._renderForCurrentMode(); - } - - private _renderForCurrentMode(): void { - if (!this._wrapper) { - return; - } - this._renderDisposables.clear(); - this._scrollableElement = undefined; - this._toolbar = undefined; - this._headerElement = undefined; - this._headerTotalCountElement = undefined; - this._chevronElement = undefined; - this._toolbarContentElement = undefined; - this._scrollableDomNode = undefined; - this._rootVerticalPadding = 0; - this._headerTotalCount = 0; - DOM.clearNode(this._wrapper); - this._render(this._wrapper, this._options); - this._setCollapsed(this._collapsed); - } - - private _totalCount() { - return derived(reader => { - this.harnessService.activeHarness.read(reader); - this.harnessService.availableHarnesses.read(reader); - const hidden = new Set(this.harnessService.getActiveDescriptor().hiddenSections ?? []); - let total = 0; - for (const config of CUSTOMIZATION_ITEMS) { - if (config.section && hidden.has(config.section)) { - continue; - } - if (config.modelSection) { - total += this.itemsModel.getCount(config.modelSection).read(reader); - } else if (config.isMcp) { - total += this.mcpService.servers.read(reader).length; - } else if (config.isPlugins) { - total += this.itemsModel.getPluginCount().read(reader); - } - } - return total; - }); - } - - private _render(parent: HTMLElement, options: IAICustomizationShortcutsWidgetOptions | undefined): void { - const container = DOM.append(parent, $('.ai-customization-toolbar')); - this._setRootPadding(container, CUSTOMIZATIONS_VERTICAL_PADDING, CUSTOMIZATIONS_VERTICAL_PADDING); - - // Header - const header = DOM.append(container, $('.ai-customization-header')); - this._headerElement = header; - header.setAttribute('role', 'button'); - header.setAttribute('aria-expanded', 'true'); - header.tabIndex = 0; - - const headerLabel = DOM.append(header, $('span.ai-customization-header-label')); - headerLabel.textContent = localize('customizations', "Customizations"); - this._headerTotalCountElement = DOM.append(header, $('span.ai-customization-header-total-count.hidden')); - - this._chevronElement = DOM.append(header, $('span.ai-customization-chevron')); - this._chevronElement.setAttribute('aria-hidden', 'true'); - this._updateChevron(); - - const totalCount = this._totalCount(); - this._renderDisposables.add(autorun(reader => { - this._headerTotalCount = totalCount.read(reader); - this._renderHeaderTotalCount(); - })); - - this._renderDisposables.add(DOM.addDisposableListener(header, DOM.EventType.CLICK, () => this._toggleCollapsed())); - this._renderDisposables.add(DOM.addDisposableListener(header, DOM.EventType.KEY_DOWN, e => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - this._toggleCollapsed(); - } - })); - - // Toolbar container - const scrollContent = $('.ai-customization-toolbar-content-scrollable'); - const toolbarContainer = DOM.append(scrollContent, $('.ai-customization-toolbar-content.sidebar-action-list')); - this._toolbarContentElement = toolbarContainer; - const scrollableElement = this._renderDisposables.add(new DomScrollableElement(scrollContent, { - horizontal: ScrollbarVisibility.Hidden, - vertical: ScrollbarVisibility.Auto, - useShadows: false, - })); - this._scrollableElement = scrollableElement; - this._scrollableDomNode = DOM.append(container, scrollableElement.getDomNode()); - - const toolbar = this._renderDisposables.add(this.instantiationService.createInstance(MenuWorkbenchToolBar, toolbarContainer, Menus.SidebarCustomizations, { - hiddenItemStrategy: HiddenItemStrategy.NoHide, - toolbarOptions: { primaryGroup: () => true }, - telemetrySource: 'sidebarCustomizations', - })); - this._toolbar = toolbar; - - // Re-layout when toolbar items change (e.g., Plugins item appearing after extension activation) - this._renderDisposables.add(toolbar.onDidChangeMenuItems(() => { - this._scrollableElement?.scanDomNode(); - this._onDidChangeHeight.fire(); - options?.onDidChangeLayout?.(); - })); - } - - get desiredHeight(): number { - const content = this._toolbarContentElement; - if (!content) { - return 0; - } - if (this._collapsed) { - return this.collapsedHeight; - } - - const headerHeight = this._headerElement?.offsetHeight ?? 0; - const height = Math.ceil(this._rootVerticalPadding + headerHeight + content.scrollHeight); - return Number.isFinite(height) ? height : 0; - } - - private _setRootPadding(element: HTMLElement, top: number, bottom: number): void { - element.style.padding = `${top}px 0 ${bottom}px 0`; - this._rootVerticalPadding = top + bottom; - } - - private _toggleCollapsed(): void { - this._setCollapsed(!this._collapsed); - this.storageService.store(CUSTOMIZATIONS_COLLAPSED_STORAGE_KEY, this._collapsed, StorageScope.PROFILE, StorageTarget.USER); - this._onDidToggleCollapsed.fire(this._collapsed); - this._onDidChangeHeight.fire(); - } - - private _setCollapsed(collapsed: boolean): void { - if (collapsed && this._scrollableDomNode?.contains(DOM.getActiveElement())) { - this._headerElement?.focus(); - } - this._collapsed = collapsed; - this._headerElement?.classList.toggle('collapsed', collapsed); - this._headerElement?.setAttribute('aria-expanded', String(!collapsed)); - if (this._scrollableDomNode) { - this._scrollableDomNode.style.display = collapsed ? 'none' : ''; - } - this._updateChevron(); - this._renderHeaderTotalCount(); - } - - private _updateChevron(): void { - if (!this._chevronElement) { - return; - } - this._chevronElement.className = 'ai-customization-chevron'; - this._chevronElement.classList.add(...ThemeIcon.asClassNameArray(this._collapsed ? Codicon.chevronRight : Codicon.chevronDown)); - } - - private _renderHeaderTotalCount(): void { - if (!this._headerTotalCountElement) { - return; - } - this._headerTotalCountElement.textContent = this._headerTotalCount > 0 ? `${this._headerTotalCount}` : ''; - this._headerTotalCountElement.classList.toggle('hidden', !this._collapsed || this._headerTotalCount === 0); - } - - layout(_height: number, _width: number): void { - if (this._collapsed) { - return; - } - this._scrollableElement?.scanDomNode(); - } - - focus(): void { - if (this._collapsed) { - this._headerElement?.focus(); - return; - } - this._toolbar?.focus(); - } -} diff --git a/src/vs/sessions/contrib/sessions/browser/customizations.contribution.ts b/src/vs/sessions/contrib/sessions/browser/customizations.contribution.ts new file mode 100644 index 00000000000000..7eaa0cc7f17214 --- /dev/null +++ b/src/vs/sessions/contrib/sessions/browser/customizations.contribution.ts @@ -0,0 +1,105 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { autorun } from '../../../../base/common/observable.js'; +import { localize2 } from '../../../../nls.js'; +import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; +import { AICustomizationManagementEditor } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.js'; +import { AICustomizationManagementEditorInput } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.js'; +import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; +import { ICustomizationHarnessService } from '../../../../workbench/contrib/chat/common/customizationHarnessService.js'; +import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; +import { ISessionsService } from '../../../services/sessions/browser/sessionsService.js'; +import { ISession } from '../../../services/sessions/common/session.js'; +import { OPEN_AI_CUSTOMIZATIONS_COMMAND_ID } from './customizationsConstants.js'; + +async function openCustomizationOverviewPage(editorService: IEditorService, harnessService: ICustomizationHarnessService, sessionsService: ISessionsService): Promise { + const session = sessionsService.activeSession.get(); + if (session) { + harnessService.setActiveSession(session.resource); + } + + const input = AICustomizationManagementEditorInput.getOrCreate(); + input.setTargetLabels(harnessService.getActiveDescriptor().label, session?.workspace.get()?.folders[0]?.name); + const pane = await editorService.openEditor(input, { pinned: true }); + if (pane instanceof AICustomizationManagementEditor) { + pane.showWelcomePage(); + } +} + +registerAction2(class extends Action2 { + constructor() { + super({ + id: OPEN_AI_CUSTOMIZATIONS_COMMAND_ID, + title: localize2('customizations', "Customizations"), + precondition: ChatContextKeys.enabled, + }); + } + + async run(accessor: ServicesAccessor): Promise { + await openCustomizationOverviewPage( + accessor.get(IEditorService), + accessor.get(ICustomizationHarnessService), + accessor.get(ISessionsService), + ); + } +}); + +/** + * Returns the harness id that matches a given session, or `undefined` if no + * harness is registered for it. + * + * The session's `resource.scheme` is the per-host harness id (e.g. local AHP + * uses `agent-host-${provider}` and remote AHP uses `remote-${authority}-${provider}`), + * while {@link ISession.sessionType} is the agent provider name shared across + * hosts (e.g. `copilotcli`). Lookup therefore prefers the resource scheme so + * that an AHP remote session selects its remote harness rather than the local + * harness with the same `sessionType`. The `sessionType` is kept as a fallback + * for harnesses whose id matches it directly. + */ +export function findHarnessIdForSession(session: ISession | undefined, harnessService: ICustomizationHarnessService): string | undefined { + if (!session) { + return undefined; + } + const schemeId = session.resource.scheme; + if (harnessService.findHarnessById(schemeId)) { + return schemeId; + } + if (harnessService.findHarnessById(session.sessionType)) { + return session.sessionType; + } + return undefined; +} + +/** + * Keeps the active customization harness in sync with the currently active + * session. This drives the customizations editor so it reflects the harness + * that matches the session the user is interacting with. + */ +export class ActiveSessionHarnessSyncContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'workbench.contrib.sessionsActiveHarnessSync'; + + constructor( + @ISessionsService sessionsService: ISessionsService, + @ICustomizationHarnessService harnessService: ICustomizationHarnessService, + ) { + super(); + + this._register(autorun(reader => { + const session = sessionsService.activeSession.read(reader); + if (!session) { + return; + } + harnessService.availableHarnesses.read(reader); + harnessService.setActiveSession(session.resource); + })); + } +} + +registerWorkbenchContribution2(ActiveSessionHarnessSyncContribution.ID, ActiveSessionHarnessSyncContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/sessions/contrib/sessions/browser/customizationsConstants.ts b/src/vs/sessions/contrib/sessions/browser/customizationsConstants.ts new file mode 100644 index 00000000000000..5063e31b354d16 --- /dev/null +++ b/src/vs/sessions/contrib/sessions/browser/customizationsConstants.ts @@ -0,0 +1,6 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export const OPEN_AI_CUSTOMIZATIONS_COMMAND_ID = 'sessions.customization.overview'; diff --git a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts b/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts deleted file mode 100644 index 38e2b4915d6dad..00000000000000 --- a/src/vs/sessions/contrib/sessions/browser/customizationsToolbar.contribution.ts +++ /dev/null @@ -1,417 +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 '../../../browser/media/sidebarActionButton.css'; -import './media/customizationsToolbar.css'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { localize } from '../../../../nls.js'; -import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js'; -import { ContextKeyExpr, ContextKeyExpression, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; -import { AICustomizationManagementEditor } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.js'; -import { AICustomizationManagementEditorInput } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.js'; -import { IAICustomizationItemsModel, ItemsModelSection } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationItemsModel.js'; -import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js'; -import { ILanguageModelToolsService } from '../../../../workbench/contrib/chat/common/tools/languageModelToolsService.js'; -import { AGENT_HOST_COPILOT_CLI_SESSION_TYPE, countEnabledCustomizationTools, IAgentHostToolSetEnablementService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostToolSetEnablementService.js'; -import { Menus } from '../../../browser/menus.js'; -import { agentIcon, instructionsIcon, mcpServerIcon, pluginIcon, skillIcon, hookIcon, toolsIcon } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationIcons.js'; -import { ActionViewItem, IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; -import { IAction } from '../../../../base/common/actions.js'; -import { $, append } from '../../../../base/browser/dom.js'; -import { autorun } from '../../../../base/common/observable.js'; -import { Button } from '../../../../base/browser/ui/button/button.js'; -import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; -import { AICustomizationManagementSection } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; -import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; -import { ICustomizationHarnessService } from '../../../../workbench/contrib/chat/common/customizationHarnessService.js'; -import { ISession } from '../../../services/sessions/common/session.js'; -import { ISessionsService } from '../../../services/sessions/browser/sessionsService.js'; -import { SessionType } from '../../../../workbench/contrib/chat/common/chatSessionsService.js'; - -export interface ICustomizationItemConfig { - readonly id: string; - readonly label: string; - readonly icon: ThemeIcon; - readonly section?: typeof AICustomizationManagementSection[keyof typeof AICustomizationManagementSection]; - /** If set, count comes from `IAICustomizationItemsModel.getCount(modelSection)`. */ - readonly modelSection?: ItemsModelSection; - readonly isMcp?: boolean; - readonly isPlugins?: boolean; - readonly isTools?: boolean; - /** Additional `when` clause beyond the standard harness-visibility gate. */ - readonly when?: ContextKeyExpression; -} - -/** - * Per-section context key indicating whether the active harness exposes - * the section in the sidebar customizations toolbar. Driven by - * `IHarnessDescriptor.hiddenSections` and consumed via the menu `when` - * clause registered alongside each customization action. - */ -function customizationSectionVisibleKey(section: string): string { - return `sessionsCustomizationSectionVisible.${section}`; -} - -const CUSTOMIZATION_OVERVIEW_ITEM: ICustomizationItemConfig = { - id: 'sessions.customization.overview', - label: localize('overview', "Overview"), - icon: Codicon.home, -}; - -export const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [ - { - id: 'sessions.customization.plugins', - label: localize('plugins', "Plugins"), - icon: pluginIcon, - section: AICustomizationManagementSection.Plugins, - isPlugins: true, - }, - { - id: 'sessions.customization.mcpServers', - label: localize('mcpServers', "MCP Servers"), - icon: mcpServerIcon, - section: AICustomizationManagementSection.McpServers, - isMcp: true, - }, - { - id: 'sessions.customization.skills', - label: localize('skills', "Skills"), - icon: skillIcon, - section: AICustomizationManagementSection.Skills, - modelSection: AICustomizationManagementSection.Skills, - }, - { - id: 'sessions.customization.instructions', - label: localize('instructions', "Instructions"), - icon: instructionsIcon, - section: AICustomizationManagementSection.Instructions, - modelSection: AICustomizationManagementSection.Instructions, - }, - { - id: 'sessions.customization.agents', - label: localize('agents', "Agents"), - icon: agentIcon, - section: AICustomizationManagementSection.Agents, - modelSection: AICustomizationManagementSection.Agents, - }, - { - id: 'sessions.customization.hooks', - label: localize('hooks', "Hooks"), - icon: hookIcon, - section: AICustomizationManagementSection.Hooks, - modelSection: AICustomizationManagementSection.Hooks, - }, - { - id: 'sessions.customization.tools', - label: localize('tools', "Tools"), - icon: toolsIcon, - section: AICustomizationManagementSection.Tools, - isTools: true, - }, - { - id: 'sessions.customization.harnessSettings', - label: localize('harnessSettings', "Codex"), - icon: Codicon.openai, - section: AICustomizationManagementSection.HarnessSettings, - }, -]; - -async function openCustomizationOverviewPage(editorService: IEditorService, harnessService: ICustomizationHarnessService, sessionsService: ISessionsService): Promise { - const session = sessionsService.activeSession.get(); - if (session) { - harnessService.setActiveSession(session.resource); - } - - const input = AICustomizationManagementEditorInput.getOrCreate(); - input.setTargetLabels(harnessService.getActiveDescriptor().label, session?.workspace.get()?.folders[0]?.name); - const pane = await editorService.openEditor(input, { pinned: true }); - if (pane instanceof AICustomizationManagementEditor) { - pane.showWelcomePage(); - } -} - -async function openCustomizationSectionPage(editorService: IEditorService, harnessService: ICustomizationHarnessService, sessionsService: ISessionsService, section: typeof AICustomizationManagementSection[keyof typeof AICustomizationManagementSection]): Promise { - const session = sessionsService.activeSession.get(); - if (session) { - harnessService.setActiveSession(session.resource); - } - - const input = AICustomizationManagementEditorInput.getOrCreate(); - input.setTargetLabels(harnessService.getActiveDescriptor().label, session?.workspace.get()?.folders[0]?.name); - const pane = await editorService.openEditor(input, { pinned: true }); - if (pane instanceof AICustomizationManagementEditor) { - pane.selectSectionById(section); - } -} - -/** - * Custom ActionViewItem for each customization link in the toolbar. - * Renders icon + label + a single count badge driven by the same - * observables that feed the customizations editor — so the badge always - * matches the editor's count exactly. - */ -export class CustomizationLinkViewItem extends ActionViewItem { - - private readonly _viewItemDisposables: DisposableStore; - private _button: Button | undefined; - private _countContainer: HTMLElement | undefined; - - constructor( - action: IAction, - options: IBaseActionViewItemOptions, - private readonly _config: ICustomizationItemConfig, - @IAICustomizationItemsModel private readonly _itemsModel: IAICustomizationItemsModel, - @IMcpService private readonly _mcpService: IMcpService, - @ILanguageModelToolsService private readonly _toolsService: ILanguageModelToolsService, - @IAgentHostToolSetEnablementService private readonly _toolEnablementService: IAgentHostToolSetEnablementService, - ) { - super(undefined, action, { ...options, icon: false, label: false }); - this._viewItemDisposables = this._register(new DisposableStore()); - } - - protected override getTooltip(): string | undefined { - return undefined; - } - - override render(container: HTMLElement): void { - super.render(container); - container.classList.add('customization-link-widget', 'sidebar-action'); - - // Button (left) - uses supportIcons to render codicon in label - const buttonContainer = append(container, $('.customization-link-button-container')); - this._button = this._viewItemDisposables.add(new Button(buttonContainer, { - ...defaultButtonStyles, - secondary: true, - title: false, - supportIcons: true, - buttonSecondaryBackground: 'transparent', - buttonSecondaryHoverBackground: undefined, - buttonSecondaryForeground: undefined, - buttonSecondaryBorder: undefined, - })); - this._button.element.classList.add('customization-link-button', 'sidebar-action-button'); - this._button.label = `$(${this._config.icon.id}) ${this._config.label}`; - - this._viewItemDisposables.add(this._button.onDidClick(() => { - this._action.run(); - })); - - // Count container (inside button, floating right) - this._countContainer = append(this._button.element, $('span.customization-link-counts')); - - this._viewItemDisposables.add(autorun(reader => { - const count = this._readCount(reader); - if (this._countContainer) { - this._renderTotalCount(this._countContainer, count); - } - })); - } - - private _readCount(reader: Parameters[0]>[0]): number { - if (this._config.modelSection) { - return this._itemsModel.getCount(this._config.modelSection).read(reader); - } - if (this._config.isMcp) { - return this._mcpService.servers.read(reader).length; - } - if (this._config.isPlugins) { - return this._itemsModel.getPluginCount().read(reader); - } - if (this._config.isTools) { - const state = this._toolEnablementService.observe(AGENT_HOST_COPILOT_CLI_SESSION_TYPE).read(reader); - const toolSets = this._toolsService.toolSets.read(reader); - return countEnabledCustomizationTools(toolSets, state, reader); - } - return 0; - } - - private _renderTotalCount(container: HTMLElement, count: number): void { - container.textContent = ''; - container.classList.toggle('hidden', count === 0); - if (count > 0) { - const badge = append(container, $('span.source-count-badge')); - const num = append(badge, $('span.source-count-num')); - num.textContent = `${count}`; - } - } -} - -// --- Register actions and view items --- // - -export class CustomizationsToolbarContribution extends Disposable implements IWorkbenchContribution { - - static readonly ID = 'workbench.contrib.sessionsCustomizationsToolbar'; - - constructor( - @IActionViewItemService actionViewItemService: IActionViewItemService, - @IInstantiationService instantiationService: IInstantiationService, - @ICustomizationHarnessService harnessService: ICustomizationHarnessService, - @IContextKeyService contextKeyService: IContextKeyService, - ) { - super(); - - // Per-section visibility context keys, kept in sync with the active - // harness's `hiddenSections`. Each customization action's menu entry - // is gated on its key so that harnesses (e.g. Claude, AHP) which - // don't support a customization type don't surface its row. - const visibilityKeys = new Map>(); - for (const config of CUSTOMIZATION_ITEMS) { - if (!config.section) { - continue; - } - const key = new RawContextKey(customizationSectionVisibleKey(config.section), true).bindTo(contextKeyService); - visibilityKeys.set(config.section, key); - } - this._register(autorun(reader => { - const activeHarness = harnessService.activeHarness.read(reader); - harnessService.availableHarnesses.read(reader); - const descriptor = harnessService.getActiveDescriptor(); - const hidden = new Set(descriptor.hiddenSections ?? []); - for (const config of CUSTOMIZATION_ITEMS) { - if (!config.section) { - continue; - } - const supported = config.section !== AICustomizationManagementSection.HarnessSettings || activeHarness === SessionType.AgentHostCodex; - visibilityKeys.get(config.section)!.set(!hidden.has(config.section) && supported); - } - })); - - this._register(actionViewItemService.register(Menus.SidebarCustomizations, CUSTOMIZATION_OVERVIEW_ITEM.id, (action, options) => { - return instantiationService.createInstance(CustomizationLinkViewItem, action, options, CUSTOMIZATION_OVERVIEW_ITEM); - }, undefined)); - - this._register(registerAction2(class extends Action2 { - constructor() { - super({ - id: CUSTOMIZATION_OVERVIEW_ITEM.id, - title: CUSTOMIZATION_OVERVIEW_ITEM.label, - precondition: ChatContextKeys.enabled, - menu: { - id: Menus.SidebarCustomizations, - group: 'navigation', - order: 0, - when: ChatContextKeys.enabled, - } - }); - } - async run(accessor: ServicesAccessor): Promise { - await openCustomizationOverviewPage( - accessor.get(IEditorService), - accessor.get(ICustomizationHarnessService), - accessor.get(ISessionsService), - ); - } - })); - - for (const [index, config] of CUSTOMIZATION_ITEMS.entries()) { - if (!config.section) { - continue; - } - const section = config.section; - // Register the custom ActionViewItem for this action - this._register(actionViewItemService.register(Menus.SidebarCustomizations, config.id, (action, options) => { - return instantiationService.createInstance(CustomizationLinkViewItem, action, options, config); - }, undefined)); - - const sectionVisibleWhen = ContextKeyExpr.has(customizationSectionVisibleKey(section)); - const combinedWhen = config.when - ? ContextKeyExpr.and(ChatContextKeys.enabled, sectionVisibleWhen, config.when) - : ContextKeyExpr.and(ChatContextKeys.enabled, sectionVisibleWhen); - - // Register the action with menu item - this._register(registerAction2(class extends Action2 { - constructor() { - super({ - id: config.id, - title: config.label, - menu: { - id: Menus.SidebarCustomizations, - group: 'navigation', - order: index + 1, - when: combinedWhen, - } - }); - } - async run(accessor: ServicesAccessor): Promise { - const editorService = accessor.get(IEditorService); - const harnessService = accessor.get(ICustomizationHarnessService); - const sessionsService = accessor.get(ISessionsService); - await openCustomizationSectionPage(editorService, harnessService, sessionsService, section); - } - })); - } - } -} - -registerWorkbenchContribution2(CustomizationsToolbarContribution.ID, CustomizationsToolbarContribution, WorkbenchPhase.AfterRestored); - -/** - * Returns the harness id that matches a given session, or `undefined` if no - * harness is registered for it. - * - * The session's `resource.scheme` is the per-host harness id (e.g. local AHP - * uses `agent-host-${provider}` and remote AHP uses `remote-${authority}-${provider}`), - * while {@link ISession.sessionType} is the agent provider name shared across - * hosts (e.g. `copilotcli`). Lookup therefore prefers the resource scheme so - * that an AHP remote session selects its remote harness rather than the local - * harness with the same `sessionType`. The `sessionType` is kept as a fallback - * for harnesses whose id matches it directly. - */ -export function findHarnessIdForSession(session: ISession | undefined, harnessService: ICustomizationHarnessService): string | undefined { - if (!session) { - return undefined; - } - const schemeId = session.resource.scheme; - if (harnessService.findHarnessById(schemeId)) { - return schemeId; - } - if (harnessService.findHarnessById(session.sessionType)) { - return session.sessionType; - } - return undefined; -} - -/** - * Keeps the active customization harness in sync with the currently active - * session. This drives the customizations sidebar (counts, filtering) and the - * customizations editor so they reflect the harness that matches the session - * the user is interacting with. - * - * This covers two cases identically: - * - opening / navigating into an existing session - * - selecting "New session in {workspace}" (which sets a pending active - * session before the user has sent the first request) - */ -export class ActiveSessionHarnessSyncContribution extends Disposable implements IWorkbenchContribution { - - static readonly ID = 'workbench.contrib.sessionsActiveHarnessSync'; - - constructor( - @ISessionsService sessionsService: ISessionsService, - @ICustomizationHarnessService harnessService: ICustomizationHarnessService, - ) { - super(); - - this._register(autorun(reader => { - const session = sessionsService.activeSession.read(reader); - if (!session) { - return; - } - // Re-read available harnesses so we re-run when an external harness - // (e.g. agent host, CLI) registers asynchronously after the session - // has already been selected. - harnessService.availableHarnesses.read(reader); - harnessService.setActiveSession(session.resource); - })); - } -} - -registerWorkbenchContribution2(ActiveSessionHarnessSyncContribution.ID, ActiveSessionHarnessSyncContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/sessions/contrib/sessions/browser/media/agentHostToolbar.css b/src/vs/sessions/contrib/sessions/browser/media/agentHostToolbar.css index ed68bcd3c3f7bc..e786dde7885ffa 100644 --- a/src/vs/sessions/contrib/sessions/browser/media/agentHostToolbar.css +++ b/src/vs/sessions/contrib/sessions/browser/media/agentHostToolbar.css @@ -4,11 +4,7 @@ *--------------------------------------------------------------------------------------------*/ /* Agent Host section - sits at the bottom of the sessions sidebar on web desktop. */ -/* - * Static divider above the section. Matches the split view separator between - * sessions and customizations — same color and same horizontal inset — but is - * a plain line, since this section cannot be resized. - */ +/* Static divider above the non-resizable section. */ .agent-host-toolbar-separator { flex-shrink: 0; height: 1px; diff --git a/src/vs/sessions/contrib/sessions/browser/media/customizationsToolbar.css b/src/vs/sessions/contrib/sessions/browser/media/customizationsToolbar.css deleted file mode 100644 index 4f8c5ab95aac3e..00000000000000 --- a/src/vs/sessions/contrib/sessions/browser/media/customizationsToolbar.css +++ /dev/null @@ -1,192 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -/* AI Customization section - pinned to bottom */ -.ai-customization-shortcuts-widget { - height: 100%; - min-height: 0; - overflow: hidden; -} - -.ai-customization-toolbar { - display: flex; - flex-direction: column; - height: 100%; - min-height: 0; - position: relative; - box-sizing: border-box; - overflow: hidden; - font-size: var(--vscode-fontSize-label1, 12px); -} - -/* Make the toolbar, action bar, and items fill full width and stack vertically */ -.ai-customization-toolbar .ai-customization-toolbar-content .monaco-toolbar, -.ai-customization-toolbar .ai-customization-toolbar-content .monaco-action-bar { - width: 100%; -} - -.ai-customization-toolbar .ai-customization-toolbar-content .monaco-action-bar .actions-container { - display: flex; - flex-direction: column; - width: 100%; -} - -.ai-customization-toolbar .ai-customization-toolbar-content .monaco-action-bar .action-item { - width: 100%; - max-width: 100%; -} - -.ai-customization-toolbar .customization-link-widget { - width: 100%; -} - -/* Customization header */ -.ai-customization-toolbar .ai-customization-header { - display: flex; - align-items: center; - flex-shrink: 0; - gap: var(--vscode-spacing-size40, 4px); - -webkit-user-select: none; - user-select: none; - padding: 6px 10px; - font-size: var(--vscode-fontSize-label1, 12px); - font-weight: var(--vscode-fontWeight-semiBold); - color: var(--vscode-foreground); - border-radius: var(--vscode-cornerRadius-medium); - cursor: pointer; -} - -.ai-customization-toolbar .ai-customization-header:hover .ai-customization-header-label, -.ai-customization-toolbar .ai-customization-header:focus-visible .ai-customization-header-label { - color: var(--vscode-strongForeground); -} - -.ai-customization-toolbar .ai-customization-header:focus-visible { - outline: 1px solid var(--vscode-focusBorder); - outline-offset: -1px; -} - -.ai-customization-toolbar .ai-customization-header:focus:not(:focus-visible) { - outline: none !important; -} - -.ai-customization-toolbar .ai-customization-header-label { - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.ai-customization-toolbar .ai-customization-header-total-count { - color: var(--vscode-descriptionForeground); - font-size: var(--vscode-fontSize-label2, 11px); - font-weight: var(--vscode-fontWeight-regular); - line-height: 1; -} - -.ai-customization-toolbar .ai-customization-header-total-count.hidden { - display: none; -} - -.ai-customization-toolbar .ai-customization-chevron { - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - color: var(--vscode-descriptionForeground); - font-size: var(--vscode-codiconFontSize-compact, 12px); - width: 22px; - height: 22px; - border-radius: var(--vscode-cornerRadius-small); - visibility: hidden; - opacity: 0; -} - -.ai-customization-toolbar .ai-customization-header:hover .ai-customization-chevron, -.ai-customization-toolbar .ai-customization-header:focus-visible .ai-customization-chevron, -.ai-customization-toolbar .ai-customization-header.collapsed .ai-customization-chevron { - visibility: visible; - opacity: 0.7; -} - -.ai-customization-toolbar .ai-customization-chevron:hover { - background-color: var(--vscode-toolbar-hoverBackground); -} - -/* Button container - fills available space. Per-item containers are inset - 10px so they sit inside the toolbar's rounded edge. */ -.ai-customization-toolbar .customization-link-button-container { - overflow: hidden; - min-width: 0; - flex: 1; - margin: 0 10px; -} - -/* Button needs relative positioning for counts overlay */ -.ai-customization-toolbar .customization-link-button { - position: relative; - font-size: var(--vscode-fontSize-label1, 12px); -} - -/* Icons use the standard icon foreground color. */ -.ai-customization-toolbar .customization-link-button .codicon { - color: var(--vscode-icon-foreground) !important; -} - -/* Match hover color used by session list items rather than the - default toolbar hover used by `.sidebar-action-button`. */ -.ai-customization-toolbar .customization-link-button.sidebar-action-button:hover { - background-color: var(--vscode-list-hoverBackground); -} - -/* Counts - floating right inside the per-item link button. Tuned to - line up visually with the header chevron above. */ -.ai-customization-toolbar .customization-link-counts { - position: absolute; - right: 10px; - top: 50%; - transform: translateY(-50%); - display: flex; - align-items: center; - text-align: right; - gap: 6px; -} - -.ai-customization-toolbar .customization-link-counts.hidden { - display: none; -} - -.ai-customization-toolbar .source-count-badge { - display: flex; - align-items: center; - gap: 2px; -} - -.ai-customization-toolbar .source-count-icon { - font-size: var(--vscode-fontSize-label2, 12px); - opacity: 0.6; -} - -.ai-customization-toolbar .source-count-num { - font-size: var(--vscode-fontSize-label2, 11px); - color: var(--vscode-descriptionForeground); - opacity: 0.8; -} - -.ai-customization-toolbar > .monaco-scrollable-element { - flex: 1; - min-height: 0; -} - -.ai-customization-toolbar .ai-customization-toolbar-content-scrollable { - height: 100%; - min-height: 100%; - overflow: hidden; - width: 100%; -} - -.ai-customization-toolbar .ai-customization-toolbar-content { - overflow: hidden; - padding-bottom: 2px; -} diff --git a/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css b/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css index c47e2094f0a52d..3d7e6619dd23bd 100644 --- a/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css +++ b/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css @@ -817,6 +817,25 @@ /* Section Header */ +.sessions-list-header { + width: 100%; + padding: var(--vscode-spacing-size100) 0 0; + box-sizing: border-box; + + .agent-sessions-header-row { + padding-left: var(--vscode-spacing-size100); + } +} + +.agent-sessions-workbench .agent-sessions-viewpane .agent-sessions-control-container .sessions-list-control .monaco-list .monaco-list-rows .monaco-list-row.sessions-list-header-row, +.agent-sessions-workbench .agent-sessions-viewpane .agent-sessions-control-container .sessions-list-control .monaco-list .monaco-list-rows .monaco-list-row.sessions-list-header-row:hover, +.agent-sessions-workbench .agent-sessions-viewpane .agent-sessions-control-container .sessions-list-control .monaco-list .monaco-list-rows .monaco-list-row.sessions-list-header-row.focused, +.agent-sessions-workbench .agent-sessions-viewpane .agent-sessions-control-container .sessions-list-control .monaco-list .monaco-list-rows .monaco-list-row.sessions-list-header-row.selected { + background-color: transparent; + outline: 0 !important; + cursor: default; +} + .session-section { display: flex; align-items: center; @@ -921,7 +940,7 @@ display: block; } -/* Shortcut sections (Automations) only show toolbar on hover, not on focus */ +/* Shortcut sections only show toolbar on hover, not on focus */ .monaco-list-row.focused .session-section.session-section-shortcut .session-section-toolbar { visibility: hidden; } @@ -929,6 +948,11 @@ display: block; visibility: hidden; } + +.session-section.session-section-shortcut { + padding-left: 0; +} + .monaco-list-row:hover .session-section.session-section-shortcut .session-section-toolbar { visibility: visible; } @@ -959,11 +983,11 @@ .sessions-list-control { /* Workspace headers and folder toggles use label color changes instead of row background fills for hover/focus/selection. */ - .monaco-list-row:has(.session-section):hover, + .monaco-list-row:has(.session-section:not(.session-section-shortcut)):hover, .monaco-list-row:has(.session-show-more-folders):hover, - .monaco-list-row.focused:has(.session-section), + .monaco-list-row.focused:has(.session-section:not(.session-section-shortcut)), .monaco-list-row.focused:has(.session-show-more-folders), - .monaco-list-row.selected:has(.session-section), + .monaco-list-row.selected:has(.session-section:not(.session-section-shortcut)), .monaco-list-row.selected:has(.session-show-more-folders) { background: transparent !important; } diff --git a/src/vs/sessions/contrib/sessions/browser/media/sessionsViewPane.css b/src/vs/sessions/contrib/sessions/browser/media/sessionsViewPane.css index ea47dc3411e23e..b2c401ad318b2d 100644 --- a/src/vs/sessions/contrib/sessions/browser/media/sessionsViewPane.css +++ b/src/vs/sessions/contrib/sessions/browser/media/sessionsViewPane.css @@ -49,7 +49,7 @@ opacity: 0.7; } - /* Sessions section - fills remaining space above customizations */ + /* Sessions section - fills the available sidebar space */ .agent-sessions-section { display: flex; flex-direction: column; @@ -88,11 +88,15 @@ display: none; } + .agent-sessions-header-container { + flex: 0 0 auto; + } + /* Header row: label + action buttons */ .agent-sessions-header-row { display: flex; align-items: center; - padding: 0 var(--vscode-spacing-size100); + padding: 0 var(--vscode-spacing-size200); height: var(--vscode-spacing-size320); box-sizing: border-box; -webkit-user-select: none; @@ -103,8 +107,8 @@ flex: 1; min-width: 0; font-size: var(--vscode-fontSize-label1, 12px); - font-weight: var(--vscode-fontWeight-semiBold, 600); - color: var(--vscode-sideBar-foreground, var(--vscode-foreground)); + font-weight: var(--vscode-fontWeight-regular, 400); + color: var(--vscode-descriptionForeground); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -172,12 +176,13 @@ .agent-sessions-control-container { flex: 1; overflow: hidden; + box-sizing: border-box; } - .agent-sessions-customizations-section { - overflow: hidden; - min-height: 0; - } +} + +.agent-sessions-workbench:not(.phone-layout) .agent-sessions-viewpane .agent-sessions-control-container { + padding-top: var(--vscode-spacing-size80); } .agent-sessions-workbench:not(.phone-layout) .agent-sessions-viewpane .agent-sessions-header-row { diff --git a/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts b/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts index 15429c07d77cd3..e62fe612f1e3e9 100644 --- a/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts +++ b/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts @@ -97,10 +97,12 @@ import { getSessionDiffStats, getSessionSummaryHoverData } from '../sessionHover import { SessionSummaryHoverWidget } from '../../../../../workbench/contrib/chat/browser/agentSessions/sessionSummaryHover.js'; import { SessionStatusIcon } from '../../../../browser/sessionStatusIcon.js'; import { ChatAutomationsEnabledContext } from '../../../../../workbench/contrib/chat/common/automations/automationsEnabled.js'; +import { ChatContextKeys } from '../../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; import { IAutomationService } from '../../../../../workbench/contrib/chat/common/automations/automationService.js'; import { ICustomViewService } from '../../../../services/customView/browser/customViewService.js'; import { AUTOMATIONS_CUSTOM_VIEW_ID } from '../automationsConstants.js'; import { AutomationsNewBadgeState, type AutomationsNewBadgeStyle } from '../automationsNewBadge.js'; +import { OPEN_AI_CUSTOMIZATIONS_COMMAND_ID } from '../customizationsConstants.js'; import { Menus } from '../../../../browser/menus.js'; import { getSessionConversationStatusAriaLabel } from '../../../../browser/sessionConversationGroups.js'; import { getAgentMergeAwarePullRequestIcon, getSessionAgentMergeConfigurationObservable, ISessionAgentMergeConfiguration, isAgentMergePullRequestIcon } from '../../../../browser/sessionAgentMerge.js'; @@ -109,6 +111,11 @@ import { BlockedSessionReason, BlockedSessions } from '../../../blockedSessions/ const $ = DOM.$; const AUTOMATIONS_SECTION_ID = 'automations'; +const CUSTOMIZATIONS_SECTION_ID = 'customizations'; +const SESSIONS_HEADER_SECTION_ID = 'sessionsHeader'; +const SESSIONS_HEADER_DEFAULT_HEIGHT = 32; +const SESSIONS_HEADER_VERTICAL_SPACING = 10; +const SESSION_SHORTCUT_SECTION_TEMPLATE_ID = 'session-shortcut-section'; const SESSION_SECTION_FOCUS_FROM_POINTER_CLASS = 'session-section-focus-from-pointer'; const SESSION_HEADER_DROP_TARGET_CLASS = 'session-header-drop-target'; /** Shared empty set used as the default "no session hierarchy is hovered/selected" value. */ @@ -163,6 +170,12 @@ export interface ISessionSection { readonly sessions: ISession[]; } +const SESSIONS_HEADER_SECTION: ISessionSection = { + id: SESSIONS_HEADER_SECTION_ID, + label: localize('sessionsHeader', "Sessions"), + sessions: [], +}; + /** * A user-created group rendered as a section-like header. Carries the backing * {@link ISessionGroup} plus its currently-visible member sessions and whether @@ -257,6 +270,8 @@ function getSessionSectionIcon(sectionId: string): ThemeIcon | undefined { return Codicon.pinned; case AUTOMATIONS_SECTION_ID: return Codicon.calendar; + case CUSTOMIZATIONS_SECTION_ID: + return Codicon.settingsGear; case 'archived': return Codicon.archive; case 'recent': @@ -270,6 +285,10 @@ function getSessionSectionIcon(sectionId: string): ThemeIcon | undefined { } } +function isShortcutSection(sectionId: string): boolean { + return sectionId === AUTOMATIONS_SECTION_ID || sectionId === CUSTOMIZATIONS_SECTION_ID; +} + function isSessionShowMore(item: SessionListItem): item is ISessionShowMore { return 'showMore' in item && (item as ISessionShowMore).showMore === true; } @@ -321,6 +340,7 @@ class SessionsTreeDelegate implements IListVirtualDelegate { */ private static readonly ITEM_HEIGHT_PHONE = 76; private static readonly SECTION_HEIGHT = 26; + private static readonly SESSIONS_HEADER_HEIGHT = SESSIONS_HEADER_DEFAULT_HEIGHT + SESSIONS_HEADER_VERTICAL_SPACING; private static readonly SHOW_MORE_HEIGHT = 26; private static readonly PLACEHOLDER_HEIGHT = 26; @@ -340,6 +360,7 @@ class SessionsTreeDelegate implements IListVirtualDelegate { */ private readonly _aggregateChatApprovals = false, private readonly _useInsetRowSpacing = false, + private readonly _sessionsHeaderHeight?: () => number, ) { } private withInsetRowSpacing(height: number): number { @@ -359,8 +380,12 @@ class SessionsTreeDelegate implements IListVirtualDelegate { chatHeight += SessionItemRenderer.getApprovalRowHeight(approval.label, this._approvalRowMaxLines) + SessionsTreeDelegate.CHAT_APPROVAL_BOTTOM_SLACK; } } + return this.withInsetRowSpacing(chatHeight); } + if (isSessionSection(element) && element.id === SESSIONS_HEADER_SECTION_ID) { + return this._sessionsHeaderHeight?.() || SessionsTreeDelegate.SESSIONS_HEADER_HEIGHT; + } if (isSessionSection(element) || isSessionGroupItem(element)) { return SessionsTreeDelegate.SECTION_HEIGHT; } @@ -416,7 +441,11 @@ class SessionsTreeDelegate implements IListVirtualDelegate { return SessionGroupRenderer.TEMPLATE_ID; } if (isSessionSection(element)) { - return SessionSectionRenderer.TEMPLATE_ID; + return element.id === SESSIONS_HEADER_SECTION_ID + ? SessionsHeaderRenderer.TEMPLATE_ID + : isShortcutSection(element.id) + ? SESSION_SHORTCUT_SECTION_TEMPLATE_ID + : SessionSectionRenderer.TEMPLATE_ID; } if (isSessionShowMore(element)) { return SessionShowMoreRenderer.TEMPLATE_ID; @@ -430,6 +459,51 @@ class SessionsTreeDelegate implements IListVirtualDelegate { //#endregion +//#region Sessions Header Renderer + +interface ISessionsHeaderTemplate { + readonly container: HTMLElement; + readonly disposables: DisposableStore; +} + +class SessionsHeaderRenderer implements ITreeRenderer { + static readonly TEMPLATE_ID = 'sessions-header'; + readonly templateId = SessionsHeaderRenderer.TEMPLATE_ID; + readonly rowClassName = 'sessions-list-header-row'; + + constructor(private readonly header: HTMLElement) { } + + renderTemplate(container: HTMLElement): ISessionsHeaderTemplate { + const disposables = new DisposableStore(); + container.classList.add('sessions-list-header'); + for (const eventType of [DOM.EventType.POINTER_DOWN, DOM.EventType.CLICK, DOM.EventType.CONTEXT_MENU]) { + disposables.add(DOM.addDisposableListener(container, eventType, event => event.stopPropagation())); + } + return { container, disposables }; + } + + renderElement(node: ITreeNode, _index: number, template: ISessionsHeaderTemplate): void { + if (isSessionSection(node.element) && node.element.id === SESSIONS_HEADER_SECTION_ID) { + template.container.append(this.header); + } + } + + disposeElement(_element: ITreeNode, _index: number, template: ISessionsHeaderTemplate): void { + if (this.header.parentElement === template.container) { + this.header.remove(); + } + } + + disposeTemplate(template: ISessionsHeaderTemplate): void { + if (this.header.parentElement === template.container) { + this.header.remove(); + } + template.disposables.dispose(); + } +} + +//#endregion + //#region Chat Item Renderer interface ISessionChatItemTemplate { @@ -1737,7 +1811,6 @@ interface ISessionSectionTemplate extends ISessionHeaderTemplate { export class SessionSectionRenderer implements ITreeRenderer { static readonly TEMPLATE_ID = 'session-section'; - readonly templateId = SessionSectionRenderer.TEMPLATE_ID; private readonly templatesByElement = new WeakMap(); private readonly templatesById = new Map(); @@ -1788,6 +1861,8 @@ export class SessionSectionRenderer implements ITreeRenderer { let label = element.label; @@ -2280,6 +2359,9 @@ class SessionsAccessibilityProvider { : label; }); } + if (isShortcutSection(element.id)) { + return element.label; + } return this.getSectionAriaLabel(element.label, element.sessions); } if (isSessionShowMore(element)) { @@ -2804,6 +2886,8 @@ export interface ISessionsListControlOptions { readonly sorting: () => SessionsSorting; readonly compact?: () => boolean; readonly findWidgetContainer?: HTMLElement; + readonly sessionsHeader?: HTMLElement; + readonly sessionsHeaderContainer?: HTMLElement; onSessionOpen(resource: URI, preserveFocus: boolean, sideBySide: boolean): void | Promise; /** @@ -3147,7 +3231,7 @@ export class SessionsList extends Disposable implements ISessionsList { .filter(blocked => blocked.reason === BlockedSessionReason.FailingCI) .map(blocked => blocked.session.sessionId) )); - const sectionRenderer = new SessionSectionRenderer( + const createSectionRenderer = (templateId?: string, rowClassName?: string) => new SessionSectionRenderer( true /* hideSectionCount */, selectHeader, showUnreadInCollapsedSections, @@ -3160,7 +3244,11 @@ export class SessionsList extends Disposable implements ISessionsList { this.uriIdentityService, this.customViewService, this.menuService, + templateId, + rowClassName, ); + const sectionRenderer = createSectionRenderer(); + const shortcutSectionRenderer = createSectionRenderer(SESSION_SHORTCUT_SECTION_TEMPLATE_ID, 'session-list-inset-row'); this._sectionRenderer = sectionRenderer; const groupRenderer = new SessionGroupRenderer({ commitEdit: (group, name) => this.commitGroupEdit(group, name), @@ -3183,6 +3271,10 @@ export class SessionsList extends Disposable implements ISessionsList { true /* useCompactQuickChatRows */, false /* aggregateChatApprovals */, true /* useInsetRowSpacing */, + () => { + const headerHeight = this.options.sessionsHeader?.offsetHeight ?? 0; + return headerHeight ? headerHeight + SESSIONS_HEADER_VERTICAL_SPACING : 0; + }, ); this._delegate = delegate; @@ -3194,13 +3286,15 @@ export class SessionsList extends Disposable implements ISessionsList { [ sessionRenderer, chatRenderer, + ...(this.options.sessionsHeader ? [new SessionsHeaderRenderer(this.options.sessionsHeader)] : []), + shortcutSectionRenderer, sectionRenderer, groupRenderer, showMoreRenderer, placeholderRenderer, ], { - accessibilityProvider: new SessionsAccessibilityProvider(sectionRenderer.automationStatus, { + accessibilityProvider: new SessionsAccessibilityProvider(shortcutSectionRenderer.automationStatus, { grouping: this.options.grouping, isPinned: session => this.isSessionPinned(session), isRenderedInCustomGroup: session => this.isRenderedInCustomGroup(session), @@ -3285,6 +3379,9 @@ export class SessionsList extends Disposable implements ISessionsList { return element.group.name; } if (isSessionSection(element)) { + if (element.id === SESSIONS_HEADER_SECTION_ID) { + return undefined; + } return element.label; } if (isSessionShowMore(element)) { @@ -3389,6 +3486,11 @@ export class SessionsList extends Disposable implements ISessionsList { this.commandService.executeCommand('sessionsView.manageAutomations'); return; } + if (isSessionSection(element) && element.id === CUSTOMIZATIONS_SECTION_ID) { + this.tree.setSelection([]); + this.commandService.executeCommand(OPEN_AI_CUSTOMIZATIONS_COMMAND_ID); + return; + } if (!isSessionSection(element) && !isSessionGroupItem(element)) { // Gate the open on workspace trust before any side effect (mark-read, // activation, folder mount). A refused open leaves the current @@ -3434,9 +3536,9 @@ export class SessionsList extends Disposable implements ISessionsList { // the `IsPhoneLayoutContext` reactive signal already maintained by // the agents workbench. const phoneKeys = new Set([IsPhoneLayoutContext.key]); - const automationKeys = new Set([ChatAutomationsEnabledContext.key]); + const shortcutKeys = new Set([ChatAutomationsEnabledContext.key, ChatContextKeys.enabled.key]); this._register(this.contextKeyService.onDidChangeContext(e => { - if (e.affectsSome(automationKeys)) { + if (e.affectsSome(shortcutKeys)) { this.update(); } if (!e.affectsSome(phoneKeys)) { @@ -3777,7 +3879,7 @@ export class SessionsList extends Disposable implements ISessionsList { }; const renderSection = (section: ISessionSection): IObjectTreeElement => { - if (section.id === AUTOMATIONS_SECTION_ID) { + if (isShortcutSection(section.id)) { return { element: section as SessionListItem, children: [], @@ -3848,6 +3950,15 @@ export class SessionsList extends Disposable implements ISessionsList { void this.automationsNewBadgeState.initialize().catch(onUnexpectedError); children.push(renderSection({ id: AUTOMATIONS_SECTION_ID, label: localize('automations', "Automations"), sessions: [] })); } + if (ChatContextKeys.enabled.getValue(this.contextKeyService) && !IsPhoneLayoutContext.getValue(this.contextKeyService)) { + children.push(renderSection({ id: CUSTOMIZATIONS_SECTION_ID, label: localize('customizations', "Customizations"), sessions: [] })); + } + const isPhone = !!IsPhoneLayoutContext.getValue(this.contextKeyService); + if (this.options.sessionsHeader && !isPhone) { + children.push({ element: SESSIONS_HEADER_SECTION }); + } else if (this.options.sessionsHeader && this.options.sessionsHeaderContainer) { + this.options.sessionsHeaderContainer.append(this.options.sessionsHeader); + } const pinnedSection = sections.find(s => s.id === 'pinned'); if (pinnedSection) { @@ -4131,6 +4242,9 @@ export class SessionsList extends Disposable implements ISessionsList { } layout(height: number, width: number): void { + if (this.options.sessionsHeader && this.tree.hasElement(SESSIONS_HEADER_SECTION)) { + this.tree.updateElementHeight(SESSIONS_HEADER_SECTION, this._delegate.getHeight(SESSIONS_HEADER_SECTION)); + } this.tree.layout(height, width); } @@ -4147,7 +4261,23 @@ export class SessionsList extends Disposable implements ISessionsList { } } + focusCustomizations(): void { + this.closeFind(); + const customizations = this.tree.getNode(null).children.find(node => + node.element && isSessionSection(node.element) && node.element.id === CUSTOMIZATIONS_SECTION_ID)?.element; + if (!customizations || !isSessionSection(customizations)) { + return; + } + this.tree.reveal(customizations); + this.tree.setFocus([customizations]); + this.tree.setSelection([customizations]); + this.tree.domFocus(); + } + openFind(): void { + if (this.tree.hasElement(SESSIONS_HEADER_SECTION)) { + this.tree.reveal(SESSIONS_HEADER_SECTION); + } this.tree.openFind(); } @@ -4551,6 +4681,9 @@ export class SessionsList extends Disposable implements ISessionsList { } if (isSessionSection(element)) { + if (element.id === SESSIONS_HEADER_SECTION_ID) { + return; + } this.showSectionContextMenu(element, e.anchor); return; } diff --git a/src/vs/sessions/contrib/sessions/browser/views/sessionsView.ts b/src/vs/sessions/contrib/sessions/browser/views/sessionsView.ts index 158fa954122970..d916be4713b056 100644 --- a/src/vs/sessions/contrib/sessions/browser/views/sessionsView.ts +++ b/src/vs/sessions/contrib/sessions/browser/views/sessionsView.ts @@ -6,8 +6,8 @@ import '../media/sessionsViewPane.css'; import * as DOM from '../../../../../base/browser/dom.js'; import { onUnexpectedError } from '../../../../../base/common/errors.js'; -import { Emitter, Event } from '../../../../../base/common/event.js'; -import { DisposableStore, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { Event } from '../../../../../base/common/event.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { autorun } from '../../../../../base/common/observable.js'; import { isWeb } from '../../../../../base/common/platform.js'; import { Orientation } from '../../../../../base/browser/ui/sash/sash.js'; @@ -30,7 +30,6 @@ import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { localize } from '../../../../../nls.js'; import { SessionsList, SessionsGrouping, SessionsSorting } from './sessionsList.js'; import { SessionStatus } from '../../../../services/sessions/common/session.js'; -import { AICustomizationShortcutsWidget } from '../aiCustomizationShortcutsWidget.js'; import { AgentHostShortcutsWidget } from '../agentHostShortcutsWidget.js'; import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { agentsBackground } from '../../../../common/theme.js'; @@ -53,7 +52,6 @@ export const SessionsViewId = 'sessions.workbench.view.sessionsView'; const GROUPING_STORAGE_KEY = 'sessionsViewPane.grouping'; const SORTING_STORAGE_KEY = 'sessionsViewPane.sorting'; const COMPACT_STORAGE_KEY = 'sessionsViewPane.compact'; -const CUSTOMIZATIONS_MIN_HEIGHT = 129; const SESSIONS_SECTION_MIN_HEIGHT = 120; const SESSIONS_HEADER_ELLIPSIS_MIN_WIDTH = 8; @@ -103,7 +101,6 @@ export class SessionsView extends ViewPane { private viewPaneContainer: HTMLElement | undefined; private sidebarSplitViewContainer: HTMLElement | undefined; private sidebarSplitView: SplitView | undefined; - private readonly customizationsPaneDisposables = this._register(new MutableDisposable()); private sessionsControlContainer: HTMLElement | undefined; private findWidgetContainer: HTMLElement | undefined; private headerRow: HTMLElement | undefined; @@ -111,7 +108,6 @@ export class SessionsView extends ViewPane { private headerActions: HTMLElement | undefined; private isFindWidgetOpen = false; sessionsControl: SessionsList | undefined; - private _customizationsWidget: AICustomizationShortcutsWidget | undefined; private currentGrouping: SessionsGrouping = SessionsGrouping.Workspace; private currentSorting: SessionsSorting = SessionsSorting.Created; private currentCompact = false; @@ -122,7 +118,6 @@ export class SessionsView extends ViewPane { private readonly filterContextKeys = new Map; getDefault: () => boolean }>(); private currentBodyHeight = 0; private currentBodyWidth = 0; - private didInitializePaneSizes = false; constructor( options: IViewPaneOptions, @@ -202,12 +197,11 @@ export class SessionsView extends ViewPane { // Sessions content container const sessionsContent = DOM.append(sessionsSection, $('.agent-sessions-content')); - // On phone, the desktop header content (label + new button + filter/find toolbar) - // is hidden in favor of the mobile filter chip row + the (+) button in the - // MobileTitlebarPart. We still create the row container because the find - // widget mounts inside it. + // The list hosts the header after its navigation shortcuts on desktop. + // This container retains it on phone, where only the find widget uses it. const phoneLayout = isPhoneLayout(this.layoutService); - const header = renderSessionsHeader(sessionsContent, phoneLayout, this.instantiationService, this.scopedContextKeyService, this._register(new DisposableStore())); + const sessionsHeaderContainer = DOM.append(sessionsContent, $('.agent-sessions-header-container')); + const header = renderSessionsHeader(sessionsHeaderContainer, phoneLayout, this.instantiationService, this.scopedContextKeyService, this._register(new DisposableStore())); const headerRow = this.headerRow = header.row; this.headerLabel = header.label; this.headerActions = header.actions; @@ -230,6 +224,8 @@ export class SessionsView extends ViewPane { sorting: () => this.currentSorting, compact: () => this.currentCompact, findWidgetContainer, + sessionsHeader: headerRow, + sessionsHeaderContainer, onSessionOpen: (resource, preserveFocus, sideBySide) => { const onOpened = () => { if (isWeb && isPhoneLayout(this.layoutService)) { @@ -344,7 +340,6 @@ export class SessionsView extends ViewPane { }; this.sidebarSplitView.addView(sessionsPane, Sizing.Distribute, 0, true); - this.updateCustomizationsPane(); const updateSplitViewStyles = () => { const borderColor = this.themeService.getColorTheme().getColor(PANEL_SECTION_BORDER); @@ -353,9 +348,9 @@ export class SessionsView extends ViewPane { updateSplitViewStyles(); this._register(this.themeService.onDidColorThemeChange(updateSplitViewStyles)); - // Agent Host toolbar (bottom, below customizations). Only rendered - // in the sessions window on web desktop layouts: electron has no - // host picker today (gated out at the menu level), phone layout + // Agent Host toolbar at the bottom. Only rendered in the sessions window + // on web desktop layouts: electron has no host picker today (gated out at + // the menu level), phone layout // uses the mobile titlebar pill instead, and auxiliary windows do // not contribute any host actions — without this gate they would // show an empty toolbar shell. @@ -374,67 +369,9 @@ export class SessionsView extends ViewPane { this._register(DOM.scheduleAtNextAnimationFrame(DOM.getWindow(parent), () => this.layoutSidebarSplitView())); } - private updateCustomizationsPane(): void { - if (!this.sidebarSplitView || !this.sidebarSplitViewContainer) { - return; - } - if (isPhoneLayout(this.layoutService)) { - if (this._customizationsWidget) { - this.sidebarSplitView.removeView(1, Sizing.Distribute); - this._customizationsWidget = undefined; - this.customizationsPaneDisposables.clear(); - } - return; - } - if (this._customizationsWidget) { - return; - } - - const store = new DisposableStore(); - this.customizationsPaneDisposables.value = store; - const customizationsSection = DOM.append(this.sidebarSplitViewContainer, $('.agent-sessions-customizations-section')); - store.add(toDisposable(() => customizationsSection.remove())); - const customizationsSizeChange = store.add(new Emitter()); - const customizationsWidget = this._customizationsWidget = store.add(this.instantiationService.createInstance(AICustomizationShortcutsWidget, customizationsSection, { - onDidChangeLayout: () => { - customizationsSizeChange.fire(); - this.layoutSidebarSplitView(); - }, - })); - const customizationsPane: IView = { - element: customizationsSection, - get minimumSize() { return customizationsWidget.collapsed ? customizationsWidget.collapsedHeight : CUSTOMIZATIONS_MIN_HEIGHT; }, - get maximumSize() { return customizationsWidget.collapsed ? customizationsWidget.collapsedHeight : Math.max(CUSTOMIZATIONS_MIN_HEIGHT, customizationsWidget.desiredHeight); }, - onDidChange: Event.map(Event.any(customizationsWidget.onDidChangeHeight, customizationsSizeChange.event), () => this.getCustomizationsPaneHeight()), - layout: height => { - customizationsSection.style.height = `${height}px`; - customizationsWidget.layout(height, this.currentBodyWidth); - }, - }; - this.sidebarSplitView.addView(customizationsPane, this.getCustomizationsPaneHeight(), 1, true); - - let savedCustomizationsPaneHeight = this.getCustomizationsPaneHeight(); - store.add(customizationsWidget.onDidToggleCollapsed(collapsed => { - if (!this.sidebarSplitView) { - return; - } - if (collapsed) { - const currentSize = this.sidebarSplitView.getViewSize(1); - if (currentSize > customizationsWidget.collapsedHeight) { - savedCustomizationsPaneHeight = currentSize; - } - this.sidebarSplitView.resizeView(1, customizationsWidget.collapsedHeight); - } else { - this.sidebarSplitView.resizeView(1, savedCustomizationsPaneHeight); - } - this.layoutSidebarSplitView(); - })); - this.didInitializePaneSizes = false; - } - focusCustomizations(): void { if (!isPhoneLayout(this.layoutService)) { - this._customizationsWidget?.focus(); + this.sessionsControl?.focusCustomizations(); } } @@ -637,7 +574,6 @@ export class SessionsView extends ViewPane { this.currentBodyHeight = height; this.currentBodyWidth = width; this.updateHeaderLayout(); - this.updateCustomizationsPane(); this.layoutSidebarSplitView(); if (this.sidebarSplitView || !this.sessionsControl || !this.sessionsControlContainer) { @@ -661,20 +597,6 @@ export class SessionsView extends ViewPane { this.sidebarSplitViewContainer.style.height = `${height}px`; } this.sidebarSplitView.layout(height); - if (!this.didInitializePaneSizes) { - this.didInitializePaneSizes = true; - if (this._customizationsWidget) { - this.sidebarSplitView.resizeView(1, this.getCustomizationsPaneHeight()); - } - } - } - - private getCustomizationsPaneHeight(): number { - if (this._customizationsWidget?.collapsed) { - return this._customizationsWidget.collapsedHeight; - } - const desiredHeight = this._customizationsWidget?.desiredHeight ?? 0; - return Math.max(CUSTOMIZATIONS_MIN_HEIGHT, Number.isFinite(desiredHeight) ? desiredHeight : 0); } override focus(): void { diff --git a/src/vs/sessions/contrib/sessions/browser/views/sessionsViewActions.ts b/src/vs/sessions/contrib/sessions/browser/views/sessionsViewActions.ts index 1aa89f0ac3684d..a8e32f4b5016d0 100644 --- a/src/vs/sessions/contrib/sessions/browser/views/sessionsViewActions.ts +++ b/src/vs/sessions/contrib/sessions/browser/views/sessionsViewActions.ts @@ -632,12 +632,13 @@ abstract class BaseArchiveSectionAction extends Action2 { id: SessionSectionToolbarMenuId, group: 'navigation', order: 1, - // Not on Done itself, and not on the "Chats" (quick chats) section. - // Also not on Automations. + // Not on Done itself, the "Chats" section, or shortcut entries. when: ContextKeyExpr.and( ContextKeyExpr.notEquals(SessionSectionTypeContext.key, 'archived'), ContextKeyExpr.notEquals(SessionSectionTypeContext.key, 'quickchats'), + ContextKeyExpr.notEquals(SessionSectionTypeContext.key, 'newSession'), ContextKeyExpr.notEquals(SessionSectionTypeContext.key, 'automations'), + ContextKeyExpr.notEquals(SessionSectionTypeContext.key, 'customizations'), ), }] }); diff --git a/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts b/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts deleted file mode 100644 index b6e504b58317b2..00000000000000 --- a/src/vs/sessions/contrib/sessions/test/browser/aiCustomizationShortcutsWidget.fixture.ts +++ /dev/null @@ -1,266 +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 { toAction } from '../../../../../base/common/actions.js'; -import { Emitter, Event } from '../../../../../base/common/event.js'; -import { DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { derived, IObservable, observableValue } from '../../../../../base/common/observable.js'; -import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { Codicon } from '../../../../../base/common/codicons.js'; -import { mock } from '../../../../../base/test/common/mock.js'; -import { IActionViewItemFactory, IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js'; -import { IMenu, IMenuActionOptions, IMenuService, isIMenuItem, MenuId, MenuItemAction, MenuRegistry, SubmenuItemAction } from '../../../../../platform/actions/common/actions.js'; -import { IMcpServer, IMcpService } from '../../../../../workbench/contrib/mcp/common/mcpTypes.js'; -import { IAgentPluginService } from '../../../../../workbench/contrib/chat/common/plugins/agentPluginService.js'; -import { ILanguageModelToolsService, IToolSet } from '../../../../../workbench/contrib/chat/common/tools/languageModelToolsService.js'; -import { IAgentHostToolSetEnablementService, IToolEnablementState } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostToolSetEnablementService.js'; -import { IAICustomizationItemsModel, ItemsModelSection } from '../../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationItemsModel.js'; -import { ICustomizationHarnessService, IHarnessDescriptor } from '../../../../../workbench/contrib/chat/common/customizationHarnessService.js'; -import { getChatSessionType } from '../../../../../workbench/contrib/chat/common/model/chatUri.js'; -import { AICustomizationManagementSection } from '../../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js'; -import { IAICustomizationListItem } from '../../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationItemSource.js'; -import { AICustomizationShortcutsWidget } from '../../browser/aiCustomizationShortcutsWidget.js'; -import { CUSTOMIZATION_ITEMS, CustomizationLinkViewItem, ICustomizationItemConfig } from '../../browser/customizationsToolbar.contribution.js'; -import { IEditorService } from '../../../../../workbench/services/editor/common/editorService.js'; -import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from '../../../../../workbench/test/browser/componentFixtures/fixtureUtils.js'; -import { Menus } from '../../../../browser/menus.js'; -import { ISessionsService } from '../../../../services/sessions/browser/sessionsService.js'; -import { IAutomationService } from '../../../../../workbench/contrib/chat/common/automations/automationService.js'; -import { URI } from '../../../../../base/common/uri.js'; - -// Ensure color registrations are loaded -import '../../../../common/theme.js'; -import '../../../../../platform/theme/common/colors/inputColors.js'; - - -// ============================================================================ -// One-time menu item registration (module-level). -// MenuRegistry.appendMenuItem does not throw on duplicates, unlike registerAction2 -// which registers global commands and throws on the second call. -// ============================================================================ - -const menuRegistrations = new DisposableStore(); -const OVERVIEW_ITEM: ICustomizationItemConfig = { - id: 'sessions.customization.overview', - label: 'Overview', - icon: Codicon.home, -}; -const SIDEBAR_ITEMS = [OVERVIEW_ITEM, ...CUSTOMIZATION_ITEMS]; -for (const [index, config] of SIDEBAR_ITEMS.entries()) { - menuRegistrations.add(MenuRegistry.appendMenuItem(Menus.SidebarCustomizations, { - command: { id: config.id, title: config.label }, - group: 'navigation', - order: index, - })); -} - -// ============================================================================ -// FixtureMenuService — reads from MenuRegistry without context-key filtering -// ============================================================================ - -class FixtureMenuService implements IMenuService { - declare readonly _serviceBrand: undefined; - - createMenu(id: MenuId): IMenu { - return { - onDidChange: Event.None, - dispose: () => { }, - getActions: () => { - const items = MenuRegistry.getMenuItems(id).filter(isIMenuItem); - items.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); - const actions = items.map(item => { - const title = typeof item.command.title === 'string' ? item.command.title : item.command.title.value; - return toAction({ id: item.command.id, label: title, run: () => { } }); - }); - return actions.length ? [['navigation', actions as unknown as (MenuItemAction | SubmenuItemAction)[]]] : []; - }, - }; - } - - getMenuActions(_id: MenuId, _contextKeyService: unknown, _options?: IMenuActionOptions) { return []; } - getMenuContexts() { return new Set(); } - resetHiddenStates() { } -} - -// ============================================================================ -// Minimal IActionViewItemService that supports register/lookUp -// ============================================================================ - -class FixtureActionViewItemService implements IActionViewItemService { - declare _serviceBrand: undefined; - - private readonly _providers = new Map(); - private readonly _onDidChange = new Emitter(); - readonly onDidChange = this._onDidChange.event; - - register(menu: MenuId, commandId: string | MenuId, provider: IActionViewItemFactory): { dispose(): void } { - const key = `${menu.id}/${commandId instanceof MenuId ? commandId.id : commandId}`; - this._providers.set(key, provider); - return { dispose: () => { this._providers.delete(key); } }; - } - - lookUp(menu: MenuId, commandId: string | MenuId): IActionViewItemFactory | undefined { - const key = `${menu.id}/${commandId instanceof MenuId ? commandId.id : commandId}`; - return this._providers.get(key); - } -} - -// ============================================================================ -// Mock IAICustomizationItemsModel — controllable per-section observables. -// This is the single source of truth for counts in both the editor and -// sidebar, so the fixture only needs to mock this one service. -// ============================================================================ - -interface ICustomizationCounts { - readonly agents?: number; - readonly skills?: number; - readonly instructions?: number; - readonly prompts?: number; - readonly hooks?: number; - readonly plugins?: number; -} - -function createMockItemsModel(counts?: ICustomizationCounts): IAICustomizationItemsModel { - const fakeItems = (n: number): readonly IAICustomizationListItem[] => - Array.from({ length: n }, (): IAICustomizationListItem => Object.create(null)); - - const sectionItems = new Map>([ - [AICustomizationManagementSection.Agents, observableValue('agentsItems', fakeItems(counts?.agents ?? 0))], - [AICustomizationManagementSection.Skills, observableValue('skillsItems', fakeItems(counts?.skills ?? 0))], - [AICustomizationManagementSection.Instructions, observableValue('instructionsItems', fakeItems(counts?.instructions ?? 0))], - [AICustomizationManagementSection.Prompts, observableValue('promptsItems', fakeItems(counts?.prompts ?? 0))], - [AICustomizationManagementSection.Hooks, observableValue('hooksItems', fakeItems(counts?.hooks ?? 0))], - ]); - const pluginCount = observableValue('pluginsCount', counts?.plugins ?? 0); - - return new class extends mock() { - override getItems(section: ItemsModelSection) { - return sectionItems.get(section)!; - } - override getCount(section: ItemsModelSection): IObservable { - const items = sectionItems.get(section)!; - return observableValue(`${section}-count`, items.get().length); - } - override getPluginCount(): IObservable { - return pluginCount; - } - }(); -} - -function createMockMcpService(serverCount: number = 0): IMcpService { - const MockServer = mock(); - const servers = observableValue('mockMcpServers', Array.from({ length: serverCount }, () => new MockServer())); - return new class extends mock() { - override readonly servers = servers; - }(); -} - -function createMockHarnessService(hiddenSections: readonly string[] = []): ICustomizationHarnessService { - const descriptor: IHarnessDescriptor = { - id: 'fixture', - label: 'Fixture', - icon: ThemeIcon.fromId('vm'), - hiddenSections, - }; - return new class extends mock() { - override readonly activeSessionResource = observableValue('mockActiveSessionResource', URI.parse(`${descriptor.id}:///session`)); - override readonly activeHarness = derived(reader => getChatSessionType(this.activeSessionResource.read(reader))); - override readonly availableHarnesses = observableValue('mockAvailableHarnesses', [descriptor]); - override findHarnessById(id: string) { return id === descriptor.id ? descriptor : undefined; } - override getActiveDescriptor() { return descriptor; } - }(); -} - -// ============================================================================ -// Render helper -// ============================================================================ - -function renderWidget(ctx: ComponentFixtureContext, options?: { mcpServerCount?: number; counts?: ICustomizationCounts; hiddenSections?: readonly string[]; height?: number }): void { - ctx.container.style.width = '300px'; - ctx.container.style.height = `${options?.height ?? 260}px`; - ctx.container.style.backgroundColor = 'var(--vscode-sideBar-background)'; - - const actionViewItemService = new FixtureActionViewItemService(); - - const instantiationService = createEditorServices(ctx.disposableStore, { - colorTheme: ctx.theme, - additionalServices: (reg) => { - registerWorkbenchServices(reg); - // Register overrides AFTER registerWorkbenchServices so they take priority - reg.defineInstance(IMenuService, new FixtureMenuService()); - reg.defineInstance(IActionViewItemService, actionViewItemService); - reg.defineInstance(IEditorService, new class extends mock() { - override readonly onDidActiveEditorChange = Event.None; - override readonly onDidVisibleEditorsChange = Event.None; - override readonly onDidEditorsChange = Event.None; - }()); - reg.defineInstance(ISessionsService, new class extends mock() { - override readonly activeSession = observableValue('mockActiveSession', undefined); - }()); - reg.defineInstance(IAICustomizationItemsModel, createMockItemsModel(options?.counts)); - reg.defineInstance(ICustomizationHarnessService, createMockHarnessService(options?.hiddenSections)); - reg.defineInstance(IMcpService, createMockMcpService(options?.mcpServerCount ?? 0)); - reg.defineInstance(IAgentPluginService, new class extends mock() { - override readonly plugins = observableValue('mockPlugins', []); - }()); - reg.defineInstance(ILanguageModelToolsService, new class extends mock() { - override readonly toolSets = observableValue>('mockToolSets', []); - }()); - reg.defineInstance(IAgentHostToolSetEnablementService, new class extends mock() { - override observe() { return observableValue('mockToolEnablement', { toolSets: new Map(), tools: new Map() }); } - }()); - reg.defineInstance(IAutomationService, new class extends mock() { - override readonly automations = observableValue('mockAutomations', []); - }()); - }, - }); - - // Register view item factories from the real CustomizationLinkViewItem - for (const config of SIDEBAR_ITEMS) { - ctx.disposableStore.add(actionViewItemService.register(Menus.SidebarCustomizations, config.id, (action, options) => { - return instantiationService.createInstance(CustomizationLinkViewItem, action, options, config); - })); - } - - ctx.disposableStore.add( - instantiationService.createInstance(AICustomizationShortcutsWidget, ctx.container, undefined) - ); -} - -// ============================================================================ -// Fixtures -// ============================================================================ - -export default defineThemedFixtureGroup({ path: 'sessions/' }, { - - Expanded: defineComponentFixture({ - labels: { kind: 'screenshot' }, - render: (ctx) => renderWidget(ctx), - }), - - MinimumHeight: defineComponentFixture({ - labels: { kind: 'screenshot' }, - render: (ctx) => renderWidget(ctx, { height: 129 }), - }), - - WithMcpServers: defineComponentFixture({ - labels: { kind: 'screenshot' }, - render: (ctx) => renderWidget(ctx, { mcpServerCount: 3 }), - }), - - MinimumHeightWithMcpServers: defineComponentFixture({ - labels: { kind: 'screenshot' }, - render: (ctx) => renderWidget(ctx, { mcpServerCount: 3, height: 129 }), - }), - - WithCounts: defineComponentFixture({ - labels: { kind: 'screenshot' }, - render: (ctx) => renderWidget(ctx, { - mcpServerCount: 2, - counts: { agents: 2, skills: 30, instructions: 16, hooks: 4 }, - }), - }), - -}); diff --git a/src/vs/sessions/contrib/sessions/test/browser/sessionsList.test.ts b/src/vs/sessions/contrib/sessions/test/browser/sessionsList.test.ts index 65e1f551a8e1ee..ab3e99ab96a8f2 100644 --- a/src/vs/sessions/contrib/sessions/test/browser/sessionsList.test.ts +++ b/src/vs/sessions/contrib/sessions/test/browser/sessionsList.test.ts @@ -35,6 +35,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../../pla import { IAutomationRun } from '../../../../../workbench/contrib/chat/common/automations/automation.js'; import { IAutomationService } from '../../../../../workbench/contrib/chat/common/automations/automationService.js'; import { ChatAutomationsEnabledContext } from '../../../../../workbench/contrib/chat/common/automations/automationsEnabled.js'; +import { ChatContextKeys } from '../../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; import { IPreferencesService, IOpenSettingsOptions } from '../../../../../workbench/services/preferences/common/preferences.js'; import { AgentMergeSessionState } from '../../../../../platform/agentHost/common/agentMerge.js'; import { getSessionChatDragData, isSessionChatDrag, SessionsDataTransfers } from '../../../../browser/dnd.js'; @@ -541,6 +542,89 @@ suite('Sessions - SessionsList', () => { }); }); + suite('shortcut entries', () => { + test('places the Sessions header after Automations and Customizations on desktop', () => { + const harness = createListHarness(disposables, [], instantiationService => { + instantiationService.stub(IContextKeyService, disposables.add(new ContextKeyService(new TestConfigurationService()))); + instantiationService.stub(IAutomationService, new class extends mock() { + override readonly automations = constObservable([]); + override readonly runs = constObservable([]); + override readonly catalogueState = constObservable('ready' as const); + }); + instantiationService.stub(ICustomViewService, new class extends mock() { + override readonly activeCustomView = constObservable(undefined); + }); + }); + const contextKeyService = harness.instantiationService.get(IContextKeyService); + ChatAutomationsEnabledContext.bindTo(contextKeyService).set(true); + ChatContextKeys.enabled.bindTo(contextKeyService).set(true); + const phoneLayout = IsPhoneLayoutContext.bindTo(contextKeyService); + const container = harness.createContainer(); + const sessionsHeaderContainer = document.createElement('div'); + const sessionsHeader = document.createElement('div'); + sessionsHeader.className = 'agent-sessions-header-row'; + sessionsHeader.textContent = 'Sessions'; + sessionsHeaderContainer.append(sessionsHeader); + container.prepend(sessionsHeaderContainer); + const list = harness.store.add(harness.instantiationService.createInstance(SessionsList, container, { + grouping: () => SessionsGrouping.Date, + sorting: () => SessionsSorting.Created, + sessionsHeader, + sessionsHeaderContainer, + onSessionOpen: () => { }, + })); + list.layout(300, 400); + + const shortcutLabels = () => Array.from(container.querySelectorAll('.session-section-shortcut .session-section-label'), element => element.textContent); + const navigationLabels = () => Array.from(container.querySelectorAll('.session-section-shortcut .session-section-label, .sessions-list-header .agent-sessions-header-row'), element => element.textContent); + list.focusCustomizations(); + sessionsHeader.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true })); + sessionsHeader.dispatchEvent(new MouseEvent('click', { bubbles: true })); + const desktop = { + labels: shortcutLabels(), + navigationLabels: navigationLabels(), + focused: container.querySelector('.monaco-list-row.focused .session-section-label')?.textContent, + shortcutsUseSessionRowFeedback: Array.from(container.querySelectorAll('.session-section-shortcut')).every(element => element.closest('.monaco-list-row')?.classList.contains('session-list-inset-row')), + headerRowHeight: (sessionsHeader.closest('.monaco-list-row') as HTMLElement | null)?.style.height, + ariaLabels: { + customizations: Array.from(container.querySelectorAll('.session-section-label')).find(element => element.textContent === 'Customizations')?.closest('.monaco-list-row')?.getAttribute('aria-label'), + sessions: sessionsHeader.closest('.monaco-list-row')?.getAttribute('aria-label'), + }, + }; + + phoneLayout.set(true); + const phone = { + labels: shortcutLabels(), + headerInTree: sessionsHeader.closest('.sessions-list-header') !== null, + }; + + phoneLayout.set(false); + list.focusCustomizations(); + const desktopAgain = { + labels: shortcutLabels(), + navigationLabels: navigationLabels(), + focused: container.querySelector('.monaco-list-row.focused .session-section-label')?.textContent, + }; + + assert.deepStrictEqual({ desktop, phone, desktopAgain }, { + desktop: { + labels: ['Automations', 'Customizations'], + navigationLabels: ['Automations', 'Customizations', 'Sessions'], + focused: 'Customizations', + shortcutsUseSessionRowFeedback: true, + headerRowHeight: '33px', + ariaLabels: { customizations: 'Customizations', sessions: 'Sessions' }, + }, + phone: { labels: ['Automations'], headerInTree: false }, + desktopAgain: { + labels: ['Automations', 'Customizations'], + navigationLabels: ['Automations', 'Customizations', 'Sessions'], + focused: 'Customizations', + }, + }); + }); + }); + suite('collapsed section status indicators', () => { const group: ISessionGroup = { id: 'group-a', name: 'Group A', createdAt: 1 }; diff --git a/src/vs/sessions/contrib/sessions/test/browser/sessionsRename.test.ts b/src/vs/sessions/contrib/sessions/test/browser/sessionsRename.test.ts index 9330b7fa033df4..a86acdfa2c4169 100644 --- a/src/vs/sessions/contrib/sessions/test/browser/sessionsRename.test.ts +++ b/src/vs/sessions/contrib/sessions/test/browser/sessionsRename.test.ts @@ -876,7 +876,7 @@ suite('Sessions rename', () => { hasDevContainerExecution: content.includes('Dev Container Agent Host sessions are enabled'), hasNoBackgroundOption: content.includes('choose no background'), hasPetAchievements: content.includes('View Achievements'), - hasSidebarCustomizations: content.includes('Chat Customizations section at the bottom of the left sidebar'), + hasSidebarCustomizations: content.includes('Customizations entry next to Automations in the left sidebar'), activeElement: mainWindow.document.activeElement, fallbackFocusCount: fallbackFocusCount(), }, { @@ -907,7 +907,7 @@ suite('Sessions rename', () => { test('omits the desktop customization focus command on phones', () => { const origin = mainWindow.document.createElement('button'); const { provider } = createHelpProvider(origin, false, true); - assert.strictEqual(provider.provideContent().includes('Chat Customizations section at the bottom of the left sidebar'), false); + assert.strictEqual(provider.provideContent().includes('Customizations entry next to Automations in the left sidebar'), false); }); test('falls back to the active session when the originating element is gone', () => { diff --git a/src/vs/sessions/contrib/sessions/test/browser/sessionsViewPane.test.ts b/src/vs/sessions/contrib/sessions/test/browser/sessionsViewPane.test.ts index 22d811c3d8fba9..e1e0a1dad32f67 100644 --- a/src/vs/sessions/contrib/sessions/test/browser/sessionsViewPane.test.ts +++ b/src/vs/sessions/contrib/sessions/test/browser/sessionsViewPane.test.ts @@ -5,14 +5,10 @@ import assert from 'assert'; import { mainWindow } from '../../../../../base/browser/window.js'; -import { SplitView, Sizing } from '../../../../../base/browser/ui/splitview/splitview.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; -import { DisposableStore, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { Workbench } from '../../../../browser/workbench.js'; -import { AICustomizationShortcutsWidget } from '../../browser/aiCustomizationShortcutsWidget.js'; -import { SessionsView } from '../../browser/views/sessionsView.js'; import '../../browser/media/sessionsViewPane.css'; const registerEditorTabHeightClass = Reflect.get(Workbench.prototype, 'registerEditorTabHeightClass') as (this: { @@ -27,86 +23,6 @@ const registerEditorTabHeightClass = Reflect.get(Workbench.prototype, 'registerE suite('Sessions - SessionsViewPane', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); - test('does not reserve customization space on phones and restores the pane on desktop', () => { - const mainContainer = mainWindow.document.createElement('div'); - mainContainer.classList.add('phone-layout'); - const container = mainWindow.document.createElement('div'); - const instantiationService = disposables.add(new TestInstantiationService()); - const customizationsPaneDisposables = disposables.add(new MutableDisposable()); - const splitView = disposables.add(new SplitView(container)); - splitView.addView({ - element: mainWindow.document.createElement('div'), - minimumSize: 120, - maximumSize: Number.POSITIVE_INFINITY, - onDidChange: Event.None, - layout: () => { }, - }, Sizing.Distribute); - splitView.layout(600); - let disposedWidgets = 0; - let focusCalls = 0; - instantiationService.stubInstance(AICustomizationShortcutsWidget, { - collapsed: false, - collapsedHeight: 30, - desiredHeight: 200, - onDidChangeHeight: Event.None, - onDidToggleCollapsed: Event.None, - layout: () => { }, - focus: () => focusCalls++, - dispose: () => disposedWidgets++, - }); - const host = { - layoutService: { mainContainer }, - instantiationService, - sidebarSplitViewContainer: container, - sidebarSplitView: splitView, - customizationsPaneDisposables, - _customizationsWidget: undefined as AICustomizationShortcutsWidget | undefined, - currentBodyWidth: 300, - currentBodyHeight: 600, - didInitializePaneSizes: false, - getCustomizationsPaneHeight: () => 200, - layoutSidebarSplitView: (): void => layoutPane.call(host), - }; - const updatePane = Reflect.get(SessionsView.prototype, 'updateCustomizationsPane') as (this: typeof host) => void; - const layoutPane = Reflect.get(SessionsView.prototype, 'layoutSidebarSplitView') as (this: typeof host) => void; - const focusCustomizations = SessionsView.prototype.focusCustomizations as (this: typeof host) => void; - const snapshot = () => ({ - panes: splitView.length, - sessionsHeight: splitView.getViewSize(0), - customizations: container.querySelectorAll('.agent-sessions-customizations-section').length, - hasWidget: !!host._customizationsWidget, - disposedWidgets, - focusCalls, - }); - - updatePane.call(host); - host.layoutSidebarSplitView(); - focusCustomizations.call(host); - const phone = snapshot(); - mainContainer.classList.remove('phone-layout'); - updatePane.call(host); - updatePane.call(host); - host.layoutSidebarSplitView(); - focusCustomizations.call(host); - const desktop = snapshot(); - mainContainer.classList.add('phone-layout'); - focusCustomizations.call(host); - updatePane.call(host); - host.layoutSidebarSplitView(); - const phoneAgain = snapshot(); - mainContainer.classList.remove('phone-layout'); - updatePane.call(host); - host.layoutSidebarSplitView(); - focusCustomizations.call(host); - - assert.deepStrictEqual({ phone, desktop, phoneAgain, desktopAgain: snapshot() }, { - phone: { panes: 1, sessionsHeight: 600, customizations: 0, hasWidget: false, disposedWidgets: 0, focusCalls: 0 }, - desktop: { panes: 2, sessionsHeight: 400, customizations: 1, hasWidget: true, disposedWidgets: 0, focusCalls: 1 }, - phoneAgain: { panes: 1, sessionsHeight: 600, customizations: 0, hasWidget: false, disposedWidgets: 1, focusCalls: 1 }, - desktopAgain: { panes: 2, sessionsHeight: 400, customizations: 1, hasWidget: true, disposedWidgets: 1, focusCalls: 2 }, - }); - }); - test('matches the default and compact editor tab heights', () => { const editorPartOptionsChanged = disposables.add(new Emitter()); let tabHeight: 'default' | 'compact' = 'default'; diff --git a/src/vs/sessions/sessions.common.main.ts b/src/vs/sessions/sessions.common.main.ts index bfba07741af78c..a638d5f71372e4 100644 --- a/src/vs/sessions/sessions.common.main.ts +++ b/src/vs/sessions/sessions.common.main.ts @@ -480,7 +480,7 @@ import './services/sessions/browser/sessionsListModelService.js'; import './services/sessions/browser/sessionGroupsService.js'; import './services/sessions/browser/sessionSectionOrderService.js'; import './services/agentHostFilter/browser/agentHostFilterService.js'; -import './contrib/sessions/browser/customizationsToolbar.contribution.js'; +import './contrib/sessions/browser/customizations.contribution.js'; import './contrib/changes/browser/changes.contribution.js'; import './contrib/codeReview/browser/codeReview.contributions.js'; import './contrib/files/browser/files.contribution.js'; From 26ce16b976620aa89887bf75aa2d9a6e37123954 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 18 Sep 2026 18:25:20 +0200 Subject: [PATCH 2/4] sessions: address sidebar navigation feedback Restore visible keyboard focus, active editor feedback, virtualized header layout, and shortcut menu behavior while using explicit renderer row classes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/sessionsChatAccessibilityHelp.ts | 2 +- .../sessions/browser/media/sessionsList.css | 33 ++++----- .../sessions/browser/views/sessionsList.ts | 40 ++++++++--- .../sessions/browser/views/sessionsView.ts | 1 + .../test/browser/sessionsList.test.ts | 72 ++++++++++++++++++- .../test/browser/sessionsRename.test.ts | 4 +- 6 files changed, 122 insertions(+), 30 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts b/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts index 273e8b6eefeff6..c6c3dc61a619fc 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionsChatAccessibilityHelp.ts @@ -122,7 +122,7 @@ export class SessionsChatAccessibilityHelp implements IAccessibleViewImplementat content.push(localize('sessionsChat.filesView', "Focus the Files Explorer view{0}.", '')); content.push(localize('sessionsChat.sessionsView', "Focus the Chat Sessions view{0}.", '')); if (!isPhoneLayout(accessor.get(IWorkbenchLayoutService))) { - content.push(localize('sessionsChat.customizations', "Focus the Customizations entry next to Automations in the left sidebar{0}.", ``)); + content.push(localize('sessionsChat.customizations', "Focus the Customizations entry in the left sidebar{0}.", ``)); } content.push(localize('sessionsChat.toggleSidePanel', "Toggle the side panel (the editor area together with the auxiliary bar) open or closed{0}.", '')); diff --git a/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css b/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css index 3d7e6619dd23bd..96f452fa6cea6e 100644 --- a/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css +++ b/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css @@ -8,13 +8,13 @@ height: 100%; min-height: 0; - .monaco-list-row.session-list-inset-row { + .monaco-list-row:is(.session-list-inset-row, .session-list-shortcut-row, .session-list-show-more-row, .session-list-show-more-folders-row) { border-radius: var(--vscode-cornerRadius-medium); margin: 0 10px; width: calc(100% - 20px); } - &.session-list-row-spacing .monaco-list-row.session-list-inset-row { + &.session-list-row-spacing .monaco-list-row:is(.session-list-inset-row, .session-list-shortcut-row, .session-list-show-more-row, .session-list-show-more-folders-row) { padding-bottom: var(--vscode-spacing-size20); background-clip: content-box; outline: 0 !important; @@ -24,7 +24,7 @@ } } - &.session-list-row-spacing .monaco-list .monaco-list-row.session-list-inset-row { + &.session-list-row-spacing .monaco-list .monaco-list-row:is(.session-list-inset-row, .session-list-shortcut-row, .session-list-show-more-row, .session-list-show-more-folders-row) { &.selected > .monaco-tl-row { outline: var(--vscode-strokeThickness) dotted var(--vscode-contrastActiveBorder, transparent); outline-offset: calc(-1 * var(--vscode-strokeThickness)); @@ -45,13 +45,13 @@ } } - &.session-list-row-spacing .monaco-list:focus .monaco-list-row.session-list-inset-row.focused > .monaco-tl-row, - .context-menu-visible &.session-list-row-spacing .monaco-list.last-focused .monaco-list-row.session-list-inset-row.focused > .monaco-tl-row { + &.session-list-row-spacing .monaco-list:focus .monaco-list-row:is(.session-list-inset-row, .session-list-shortcut-row, .session-list-show-more-row, .session-list-show-more-folders-row).focused > .monaco-tl-row, + .context-menu-visible &.session-list-row-spacing .monaco-list.last-focused .monaco-list-row:is(.session-list-inset-row, .session-list-shortcut-row, .session-list-show-more-row, .session-list-show-more-folders-row).focused > .monaco-tl-row { outline: var(--vscode-strokeThickness) solid var(--vscode-list-focusOutline); outline-offset: calc(-1 * var(--vscode-strokeThickness)); } - &.session-list-row-spacing .monaco-list:focus .monaco-list-row.session-list-inset-row.focused.selected > .monaco-tl-row { + &.session-list-row-spacing .monaco-list:focus .monaco-list-row:is(.session-list-inset-row, .session-list-shortcut-row, .session-list-show-more-row, .session-list-show-more-folders-row).focused.selected > .monaco-tl-row { outline-color: var(--vscode-list-focusAndSelectionOutline, var(--vscode-contrastActiveBorder, var(--vscode-list-focusOutline))); } @@ -832,7 +832,6 @@ .agent-sessions-workbench .agent-sessions-viewpane .agent-sessions-control-container .sessions-list-control .monaco-list .monaco-list-rows .monaco-list-row.sessions-list-header-row.focused, .agent-sessions-workbench .agent-sessions-viewpane .agent-sessions-control-container .sessions-list-control .monaco-list .monaco-list-rows .monaco-list-row.sessions-list-header-row.selected { background-color: transparent; - outline: 0 !important; cursor: default; } @@ -983,12 +982,12 @@ .sessions-list-control { /* Workspace headers and folder toggles use label color changes instead of row background fills for hover/focus/selection. */ - .monaco-list-row:has(.session-section:not(.session-section-shortcut)):hover, - .monaco-list-row:has(.session-show-more-folders):hover, - .monaco-list-row.focused:has(.session-section:not(.session-section-shortcut)), - .monaco-list-row.focused:has(.session-show-more-folders), - .monaco-list-row.selected:has(.session-section:not(.session-section-shortcut)), - .monaco-list-row.selected:has(.session-show-more-folders) { + .monaco-list-row.session-list-section-row:hover, + .monaco-list-row.session-list-show-more-folders-row:hover, + .monaco-list-row.session-list-section-row.focused, + .monaco-list-row.session-list-show-more-folders-row.focused, + .monaco-list-row.session-list-section-row.selected, + .monaco-list-row.session-list-show-more-folders-row.selected { background: transparent !important; } @@ -1006,8 +1005,10 @@ } &.session-section-focus-from-pointer { - .monaco-list-row.focused:has(.session-section), - .monaco-list-row.selected:has(.session-section) { + .monaco-list-row.session-list-section-row.focused, + .monaco-list-row.session-list-section-row.selected, + .monaco-list-row.session-list-shortcut-row.focused, + .monaco-list-row.session-list-shortcut-row.selected { outline: none !important; } } @@ -1145,7 +1146,7 @@ * keep these rules in sync if paddings/font sizes change here. */ .agent-sessions-workbench.phone-layout .sessions-list-control { - .monaco-list-row.session-list-inset-row { + .monaco-list-row:is(.session-list-inset-row, .session-list-shortcut-row, .session-list-show-more-row, .session-list-show-more-folders-row) { /* Horizontal-only margin: virtual list rows are absolutely * positioned with JS-set `top`/`height`, so vertical margins * here would not produce reliable inter-row spacing. Any gap diff --git a/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts b/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts index e62fe612f1e3e9..735facd053e40a 100644 --- a/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts +++ b/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts @@ -19,7 +19,7 @@ import { HighlightedLabel } from '../../../../../base/browser/ui/highlightedlabe import { createMatches, FuzzyScore, IMatch } from '../../../../../base/common/filters.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { constObservable, IObservable, IReader, ISettableObservable, autorun, derived, observableSignalFromEvent, observableValue } from '../../../../../base/common/observable.js'; +import { constObservable, IObservable, IReader, ISettableObservable, autorun, derived, observableFromEvent, observableSignalFromEvent, observableValue } from '../../../../../base/common/observable.js'; import { ThemeIcon, themeColorFromId } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { fromNow } from '../../../../../base/common/date.js'; @@ -98,7 +98,9 @@ import { SessionSummaryHoverWidget } from '../../../../../workbench/contrib/chat import { SessionStatusIcon } from '../../../../browser/sessionStatusIcon.js'; import { ChatAutomationsEnabledContext } from '../../../../../workbench/contrib/chat/common/automations/automationsEnabled.js'; import { ChatContextKeys } from '../../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; +import { AICustomizationManagementEditorInput } from '../../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.js'; import { IAutomationService } from '../../../../../workbench/contrib/chat/common/automations/automationService.js'; +import { IEditorService } from '../../../../../workbench/services/editor/common/editorService.js'; import { ICustomViewService } from '../../../../services/customView/browser/customViewService.js'; import { AUTOMATIONS_CUSTOM_VIEW_ID } from '../automationsConstants.js'; import { AutomationsNewBadgeState, type AutomationsNewBadgeStyle } from '../automationsNewBadge.js'; @@ -116,6 +118,7 @@ const SESSIONS_HEADER_SECTION_ID = 'sessionsHeader'; const SESSIONS_HEADER_DEFAULT_HEIGHT = 32; const SESSIONS_HEADER_VERTICAL_SPACING = 10; const SESSION_SHORTCUT_SECTION_TEMPLATE_ID = 'session-shortcut-section'; +const SESSION_SHOW_MORE_FOLDERS_TEMPLATE_ID = 'session-show-more-folders'; const SESSION_SECTION_FOCUS_FROM_POINTER_CLASS = 'session-section-focus-from-pointer'; const SESSION_HEADER_DROP_TARGET_CLASS = 'session-header-drop-target'; /** Shared empty set used as the default "no session hierarchy is hovered/selected" value. */ @@ -448,7 +451,7 @@ class SessionsTreeDelegate implements IListVirtualDelegate { : SessionSectionRenderer.TEMPLATE_ID; } if (isSessionShowMore(element)) { - return SessionShowMoreRenderer.TEMPLATE_ID; + return element.kind === 'folders' ? SESSION_SHOW_MORE_FOLDERS_TEMPLATE_ID : SessionShowMoreRenderer.TEMPLATE_ID; } if (isSessionPlaceholder(element)) { return SessionPlaceholderRenderer.TEMPLATE_ID; @@ -471,7 +474,10 @@ class SessionsHeaderRenderer implements ITreeRenderer void, + ) { } renderTemplate(container: HTMLElement): ISessionsHeaderTemplate { const disposables = new DisposableStore(); @@ -485,6 +491,7 @@ class SessionsHeaderRenderer implements ITreeRenderer, _index: number, template: ISessionsHeaderTemplate): void { if (isSessionSection(node.element) && node.element.id === SESSIONS_HEADER_SECTION_ID) { template.container.append(this.header); + this.layoutHeader(); } } @@ -1860,6 +1867,7 @@ export class SessionSectionRenderer implements ITreeRenderer, private readonly uriIdentityService: IUriIdentityService, private readonly customViewService: ICustomViewService, + private readonly customizationsActive: IObservable, private readonly menuService: IMenuService, readonly templateId = SessionSectionRenderer.TEMPLATE_ID, readonly rowClassName?: string, @@ -1952,6 +1960,11 @@ export class SessionSectionRenderer implements ITreeRenderer { + template.container.classList.toggle('active', this.customizationsActive.read(reader)); + })); + } this.updateChevron(template, node.collapsible, node.collapsed); @@ -2227,8 +2240,11 @@ class SessionGroupRenderer implements ITreeRenderer { static readonly TEMPLATE_ID = 'session-show-more'; - readonly templateId = SessionShowMoreRenderer.TEMPLATE_ID; - readonly rowClassName = 'session-list-inset-row'; + + constructor( + readonly templateId = SessionShowMoreRenderer.TEMPLATE_ID, + readonly rowClassName = 'session-list-show-more-row', + ) { } renderTemplate(container: HTMLElement): HTMLElement { container.classList.add('session-show-more'); @@ -2888,6 +2904,7 @@ export interface ISessionsListControlOptions { readonly findWidgetContainer?: HTMLElement; readonly sessionsHeader?: HTMLElement; readonly sessionsHeaderContainer?: HTMLElement; + readonly layoutSessionsHeader?: () => void; onSessionOpen(resource: URI, preserveFocus: boolean, sideBySide: boolean): void | Promise; /** @@ -3117,6 +3134,7 @@ export class SessionsList extends Disposable implements ISessionsList { @IOpenerService private readonly openerService: IOpenerService, @ILabelService private readonly labelService: ILabelService, @IPreferencesService private readonly preferencesService: IPreferencesService, + @IEditorService editorService: IEditorService, ) { super(); this.automationsNewBadgeState = this._register(instantiationService.createInstance(AutomationsNewBadgeState)); @@ -3215,6 +3233,7 @@ export class SessionsList extends Disposable implements ISessionsList { this._sessionRenderer = sessionRenderer; const showMoreRenderer = new SessionShowMoreRenderer(); + const showMoreFoldersRenderer = new SessionShowMoreRenderer(SESSION_SHOW_MORE_FOLDERS_TEMPLATE_ID, 'session-list-show-more-folders-row'); const placeholderRenderer = new SessionPlaceholderRenderer(hoverService); const chatRenderer = new SessionChatItemRenderer(hoverService, instantiationService, this._sessionsManagementService, this.contextViewService, markdownRendererService, approvalModel, DEFAULT_APPROVAL_ROW_MAX_LINES, item => this.preservePendingOpenFocus(item.session, item.chat), () => this.tree.domFocus(), this.activeGuideSessionIds); this._chatRenderer = chatRenderer; @@ -3231,6 +3250,7 @@ export class SessionsList extends Disposable implements ISessionsList { .filter(blocked => blocked.reason === BlockedSessionReason.FailingCI) .map(blocked => blocked.session.sessionId) )); + const customizationsActive = observableFromEvent(this, editorService.onDidActiveEditorChange, () => editorService.activeEditor instanceof AICustomizationManagementEditorInput); const createSectionRenderer = (templateId?: string, rowClassName?: string) => new SessionSectionRenderer( true /* hideSectionCount */, selectHeader, @@ -3243,12 +3263,13 @@ export class SessionsList extends Disposable implements ISessionsList { this.automationsNewBadgeState.presentation, this.uriIdentityService, this.customViewService, + customizationsActive, this.menuService, templateId, rowClassName, ); - const sectionRenderer = createSectionRenderer(); - const shortcutSectionRenderer = createSectionRenderer(SESSION_SHORTCUT_SECTION_TEMPLATE_ID, 'session-list-inset-row'); + const sectionRenderer = createSectionRenderer(undefined, 'session-list-section-row'); + const shortcutSectionRenderer = createSectionRenderer(SESSION_SHORTCUT_SECTION_TEMPLATE_ID, 'session-list-shortcut-row'); this._sectionRenderer = sectionRenderer; const groupRenderer = new SessionGroupRenderer({ commitEdit: (group, name) => this.commitGroupEdit(group, name), @@ -3286,11 +3307,12 @@ export class SessionsList extends Disposable implements ISessionsList { [ sessionRenderer, chatRenderer, - ...(this.options.sessionsHeader ? [new SessionsHeaderRenderer(this.options.sessionsHeader)] : []), + ...(this.options.sessionsHeader ? [new SessionsHeaderRenderer(this.options.sessionsHeader, () => this.options.layoutSessionsHeader?.())] : []), shortcutSectionRenderer, sectionRenderer, groupRenderer, showMoreRenderer, + showMoreFoldersRenderer, placeholderRenderer, ], { @@ -4681,7 +4703,7 @@ export class SessionsList extends Disposable implements ISessionsList { } if (isSessionSection(element)) { - if (element.id === SESSIONS_HEADER_SECTION_ID) { + if (element.id === SESSIONS_HEADER_SECTION_ID || isShortcutSection(element.id)) { return; } this.showSectionContextMenu(element, e.anchor); diff --git a/src/vs/sessions/contrib/sessions/browser/views/sessionsView.ts b/src/vs/sessions/contrib/sessions/browser/views/sessionsView.ts index d916be4713b056..fface10357a622 100644 --- a/src/vs/sessions/contrib/sessions/browser/views/sessionsView.ts +++ b/src/vs/sessions/contrib/sessions/browser/views/sessionsView.ts @@ -226,6 +226,7 @@ export class SessionsView extends ViewPane { findWidgetContainer, sessionsHeader: headerRow, sessionsHeaderContainer, + layoutSessionsHeader: () => this.updateHeaderLayout(), onSessionOpen: (resource, preserveFocus, sideBySide) => { const onOpened = () => { if (isWeb && isPhoneLayout(this.layoutService)) { diff --git a/src/vs/sessions/contrib/sessions/test/browser/sessionsList.test.ts b/src/vs/sessions/contrib/sessions/test/browser/sessionsList.test.ts index ab3e99ab96a8f2..b058a938c15666 100644 --- a/src/vs/sessions/contrib/sessions/test/browser/sessionsList.test.ts +++ b/src/vs/sessions/contrib/sessions/test/browser/sessionsList.test.ts @@ -24,6 +24,7 @@ import { TestConfigurationService } from '../../../../../platform/configuration/ import { ContextKeyService } from '../../../../../platform/contextkey/browser/contextKeyService.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IConfigurationChangeEvent, IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { NullHoverService } from '../../../../../platform/hover/test/browser/nullHoverService.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; @@ -36,7 +37,9 @@ import { IAutomationRun } from '../../../../../workbench/contrib/chat/common/aut import { IAutomationService } from '../../../../../workbench/contrib/chat/common/automations/automationService.js'; import { ChatAutomationsEnabledContext } from '../../../../../workbench/contrib/chat/common/automations/automationsEnabled.js'; import { ChatContextKeys } from '../../../../../workbench/contrib/chat/common/actions/chatContextKeys.js'; +import { AICustomizationManagementEditorInput } from '../../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.js'; import { IPreferencesService, IOpenSettingsOptions } from '../../../../../workbench/services/preferences/common/preferences.js'; +import { IEditorService } from '../../../../../workbench/services/editor/common/editorService.js'; import { AgentMergeSessionState } from '../../../../../platform/agentHost/common/agentMerge.js'; import { getSessionChatDragData, isSessionChatDrag, SessionsDataTransfers } from '../../../../browser/dnd.js'; import { IsPhoneLayoutContext, IsQuickChatSessionContext, SessionIsArchivedContext, SessionSupportsMultipleChatsContext } from '../../../../common/contextkeys.js'; @@ -149,6 +152,7 @@ suite('Sessions - SessionsList', () => { override readonly extUri = new ExtUri(() => true); }, new class extends mock() { }, + constObservable(false), new class extends mock() { }, ); const container = document.createElement('div'); @@ -206,6 +210,7 @@ suite('Sessions - SessionsList', () => { new class extends mock() { override readonly activeCustomView = constObservable(undefined); }, + constObservable(false), new class extends mock() { }, ); const container = document.createElement('div'); @@ -266,6 +271,7 @@ suite('Sessions - SessionsList', () => { new class extends mock() { override readonly activeCustomView = constObservable(undefined); }, + constObservable(false), new class extends mock() { }, ); const container = document.createElement('div'); @@ -439,6 +445,7 @@ suite('Sessions - SessionsList', () => { constObservable(undefined), uriIdentityService, new class extends mock() { }, + constObservable(false), new class extends mock() { }, ); const runResource = URI.parse('test-session:/workspace/automation'); @@ -509,6 +516,7 @@ suite('Sessions - SessionsList', () => { constObservable(undefined), uriIdentityService, new class extends mock() { }, + constObservable(false), new class extends mock() { }, ); runs.set([ @@ -544,6 +552,9 @@ suite('Sessions - SessionsList', () => { suite('shortcut entries', () => { test('places the Sessions header after Automations and Customizations on desktop', () => { + const activeEditorChanged = disposables.add(new Emitter()); + let activeEditor: AICustomizationManagementEditorInput | undefined; + let contextMenuCount = 0; const harness = createListHarness(disposables, [], instantiationService => { instantiationService.stub(IContextKeyService, disposables.add(new ContextKeyService(new TestConfigurationService()))); instantiationService.stub(IAutomationService, new class extends mock() { @@ -554,6 +565,17 @@ suite('Sessions - SessionsList', () => { instantiationService.stub(ICustomViewService, new class extends mock() { override readonly activeCustomView = constObservable(undefined); }); + instantiationService.stub(IEditorService, new class extends mock() { + override readonly onDidActiveEditorChange = activeEditorChanged.event; + override get activeEditor(): AICustomizationManagementEditorInput | undefined { + return activeEditor; + } + }); + instantiationService.stub(IContextMenuService, new class extends mock() { + override showContextMenu(): void { + contextMenuCount++; + } + }); }); const contextKeyService = harness.instantiationService.get(IContextKeyService); ChatAutomationsEnabledContext.bindTo(contextKeyService).set(true); @@ -566,26 +588,43 @@ suite('Sessions - SessionsList', () => { sessionsHeader.textContent = 'Sessions'; sessionsHeaderContainer.append(sessionsHeader); container.prepend(sessionsHeaderContainer); + let headerLayoutCount = 0; const list = harness.store.add(harness.instantiationService.createInstance(SessionsList, container, { grouping: () => SessionsGrouping.Date, sorting: () => SessionsSorting.Created, sessionsHeader, sessionsHeaderContainer, + layoutSessionsHeader: () => headerLayoutCount++, onSessionOpen: () => { }, })); list.layout(300, 400); const shortcutLabels = () => Array.from(container.querySelectorAll('.session-section-shortcut .session-section-label'), element => element.textContent); const navigationLabels = () => Array.from(container.querySelectorAll('.session-section-shortcut .session-section-label, .sessions-list-header .agent-sessions-header-row'), element => element.textContent); + const customizationsSection = () => Array.from(container.querySelectorAll('.session-section-shortcut')).find(element => element.querySelector('.session-section-label')?.textContent === 'Customizations'); list.focusCustomizations(); sessionsHeader.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true })); sessionsHeader.dispatchEvent(new MouseEvent('click', { bubbles: true })); + for (const shortcut of container.querySelectorAll('.session-section-shortcut')) { + shortcut.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, button: 2 })); + } + const customizationInput = disposables.add(AICustomizationManagementEditorInput.getOrCreate()); + const customizationsActiveBeforeOpen = customizationsSection()?.classList.contains('active'); + activeEditor = customizationInput; + activeEditorChanged.fire(); + const customizationsActiveWhileOpen = customizationsSection()?.classList.contains('active'); + activeEditor = undefined; + activeEditorChanged.fire(); + const customizationsActiveAfterClose = customizationsSection()?.classList.contains('active'); + const initialHeaderLayoutCount = headerLayoutCount; const desktop = { labels: shortcutLabels(), navigationLabels: navigationLabels(), focused: container.querySelector('.monaco-list-row.focused .session-section-label')?.textContent, - shortcutsUseSessionRowFeedback: Array.from(container.querySelectorAll('.session-section-shortcut')).every(element => element.closest('.monaco-list-row')?.classList.contains('session-list-inset-row')), + shortcutsUseOwnRowClass: Array.from(container.querySelectorAll('.session-section-shortcut')).every(element => element.closest('.monaco-list-row')?.classList.contains('session-list-shortcut-row')), headerRowHeight: (sessionsHeader.closest('.monaco-list-row') as HTMLElement | null)?.style.height, + customizationsActive: [customizationsActiveBeforeOpen, customizationsActiveWhileOpen, customizationsActiveAfterClose], + shortcutContextMenus: contextMenuCount, ariaLabels: { customizations: Array.from(container.querySelectorAll('.session-section-label')).find(element => element.textContent === 'Customizations')?.closest('.monaco-list-row')?.getAttribute('aria-label'), sessions: sessionsHeader.closest('.monaco-list-row')?.getAttribute('aria-label'), @@ -604,6 +643,7 @@ suite('Sessions - SessionsList', () => { labels: shortcutLabels(), navigationLabels: navigationLabels(), focused: container.querySelector('.monaco-list-row.focused .session-section-label')?.textContent, + headerLayoutRecomputed: headerLayoutCount > initialHeaderLayoutCount, }; assert.deepStrictEqual({ desktop, phone, desktopAgain }, { @@ -611,8 +651,10 @@ suite('Sessions - SessionsList', () => { labels: ['Automations', 'Customizations'], navigationLabels: ['Automations', 'Customizations', 'Sessions'], focused: 'Customizations', - shortcutsUseSessionRowFeedback: true, + shortcutsUseOwnRowClass: true, headerRowHeight: '33px', + customizationsActive: [false, true, false], + shortcutContextMenus: 0, ariaLabels: { customizations: 'Customizations', sessions: 'Sessions' }, }, phone: { labels: ['Automations'], headerInTree: false }, @@ -620,9 +662,35 @@ suite('Sessions - SessionsList', () => { labels: ['Automations', 'Customizations'], navigationLabels: ['Automations', 'Customizations', 'Sessions'], focused: 'Customizations', + headerLayoutRecomputed: true, }, }); }); + + test('marks regular section and folder show-more rows with renderer classes', () => { + const sessions = Array.from({ length: 6 }, (_, index) => { + const session = createTestSession(`session-${index}`, { + workspaceLabel: `Workspace ${index}`, + }).session; + return { ...session, updatedAt: constObservable(new Date(index)) }; + }); + const harness = createListHarness(disposables, sessions); + const container = harness.createContainer(); + const list = harness.store.add(harness.instantiationService.createInstance(SessionsList, container, { + grouping: () => SessionsGrouping.Workspace, + sorting: () => SessionsSorting.Created, + onSessionOpen: () => { }, + })); + list.layout(1000, 400); + + assert.deepStrictEqual({ + sectionRows: container.querySelectorAll('.monaco-list-row.session-list-section-row').length, + showMoreFoldersRows: container.querySelectorAll('.monaco-list-row.session-list-show-more-folders-row').length, + }, { + sectionRows: 1, + showMoreFoldersRows: 1, + }); + }); }); suite('collapsed section status indicators', () => { diff --git a/src/vs/sessions/contrib/sessions/test/browser/sessionsRename.test.ts b/src/vs/sessions/contrib/sessions/test/browser/sessionsRename.test.ts index a86acdfa2c4169..0752fab6c4ce52 100644 --- a/src/vs/sessions/contrib/sessions/test/browser/sessionsRename.test.ts +++ b/src/vs/sessions/contrib/sessions/test/browser/sessionsRename.test.ts @@ -876,7 +876,7 @@ suite('Sessions rename', () => { hasDevContainerExecution: content.includes('Dev Container Agent Host sessions are enabled'), hasNoBackgroundOption: content.includes('choose no background'), hasPetAchievements: content.includes('View Achievements'), - hasSidebarCustomizations: content.includes('Customizations entry next to Automations in the left sidebar'), + hasSidebarCustomizations: content.includes('Customizations entry in the left sidebar'), activeElement: mainWindow.document.activeElement, fallbackFocusCount: fallbackFocusCount(), }, { @@ -907,7 +907,7 @@ suite('Sessions rename', () => { test('omits the desktop customization focus command on phones', () => { const origin = mainWindow.document.createElement('button'); const { provider } = createHelpProvider(origin, false, true); - assert.strictEqual(provider.provideContent().includes('Customizations entry next to Automations in the left sidebar'), false); + assert.strictEqual(provider.provideContent().includes('Customizations entry in the left sidebar'), false); }); test('falls back to the active session when the originating element is gone', () => { From 0425ea4c6beeb5c541d354e81e8fa8a93188048e Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Fri, 18 Sep 2026 19:12:09 +0200 Subject: [PATCH 3/4] sessions: register editor service in list fixture Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../componentFixtures/sessions/sessionsList.fixture.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/test/browser/componentFixtures/sessions/sessionsList.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/sessions/sessionsList.fixture.ts index 39b13db1ca3f1e..3d078cb868b163 100644 --- a/src/vs/workbench/test/browser/componentFixtures/sessions/sessionsList.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/sessions/sessionsList.fixture.ts @@ -85,6 +85,7 @@ import { IChatService } from '../../../../contrib/chat/common/chatService/chatSe import { IChatModel } from '../../../../contrib/chat/common/model/chatModel.js'; import { IVoicePlaybackService } from '../../../../contrib/chat/common/voicePlaybackService.js'; import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; +import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ILifecycleService, LifecyclePhase } from '../../../../services/lifecycle/common/lifecycle.js'; import { TestProductService } from '../../../common/workbenchTestServices.js'; import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from '../fixtureUtils.js'; @@ -282,6 +283,9 @@ async function renderSessionsList(ctx: ComponentFixtureContext, options: IRender additionalServices: reg => { registerWorkbenchServices(reg); reg.defineInstance(IProductService, TestProductService); + reg.defineInstance(IEditorService, new class extends mock() { + override readonly onDidActiveEditorChange = Event.None; + }()); const reducedMotion = options.reducedMotion; if (reducedMotion !== undefined) { reg.defineInstance(IAccessibilityService, new class extends TestAccessibilityService { From 20bb72e54dee9f2beaf6cf926d4bb206f416fec3 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Sun, 20 Sep 2026 10:18:50 +0200 Subject: [PATCH 4/4] sessions: update sidebar CI baselines Make the Sessions header height assertion tolerate Chromium's platform-specific line-box rounding and accept the reviewed component fixture screenshots. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../test/browser/sessionsList.test.ts | 4 +- .../blocks-ci-screenshots.md | 48 +++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/vs/sessions/contrib/sessions/test/browser/sessionsList.test.ts b/src/vs/sessions/contrib/sessions/test/browser/sessionsList.test.ts index b058a938c15666..b1fed66df47fba 100644 --- a/src/vs/sessions/contrib/sessions/test/browser/sessionsList.test.ts +++ b/src/vs/sessions/contrib/sessions/test/browser/sessionsList.test.ts @@ -622,7 +622,7 @@ suite('Sessions - SessionsList', () => { navigationLabels: navigationLabels(), focused: container.querySelector('.monaco-list-row.focused .session-section-label')?.textContent, shortcutsUseOwnRowClass: Array.from(container.querySelectorAll('.session-section-shortcut')).every(element => element.closest('.monaco-list-row')?.classList.contains('session-list-shortcut-row')), - headerRowHeight: (sessionsHeader.closest('.monaco-list-row') as HTMLElement | null)?.style.height, + headerRowUsesPlatformMeasuredHeight: ['32px', '33px'].includes((sessionsHeader.closest('.monaco-list-row') as HTMLElement | null)?.style.height ?? ''), customizationsActive: [customizationsActiveBeforeOpen, customizationsActiveWhileOpen, customizationsActiveAfterClose], shortcutContextMenus: contextMenuCount, ariaLabels: { @@ -652,7 +652,7 @@ suite('Sessions - SessionsList', () => { navigationLabels: ['Automations', 'Customizations', 'Sessions'], focused: 'Customizations', shortcutsUseOwnRowClass: true, - headerRowHeight: '33px', + headerRowUsesPlatformMeasuredHeight: true, customizationsActive: [false, true, false], shortcutContextMenus: 0, ariaLabels: { customizations: 'Customizations', sessions: 'Sessions' }, diff --git a/test/componentFixtures/blocks-ci-screenshots.md b/test/componentFixtures/blocks-ci-screenshots.md index 73902b99e1d4be..4bb7545f841b7c 100644 --- a/test/componentFixtures/blocks-ci-screenshots.md +++ b/test/componentFixtures/blocks-ci-screenshots.md @@ -418,58 +418,58 @@ ![screenshot](https://hediet-screenshots.azurewebsites.net/images/6d74cdd3edb6fea60ae08e233a168d1ab2862b5b283206e27f77ce980e8bc79c) #### sessions/sessionsList/SessionsList_AutomationsNewBadge_Accent/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/b0e40df368cf7fc0c277fe259ffdc0d1e115c8bb247076c41e45326fe0727f27) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/382954fbded6e97bd0b9e3564292bbd42d2083f74060b89648aa169b52af582b) #### sessions/sessionsList/SessionsList_AutomationsNewBadge_Accent/DarkHighContrast -![screenshot](https://hediet-screenshots.azurewebsites.net/images/2e624aeed24ef3d79e54a89d5e03ac4b2cd40ec4c7e35bdca430bc5729e36449) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/f6bbb99b9daf32fd92e5d5d9576936c6bb572f7661e14a2a47d0859d8885a1aa) #### sessions/sessionsList/SessionsList_AutomationsNewBadge_Accent/Light -![screenshot](https://hediet-screenshots.azurewebsites.net/images/7f23bf5ec8cc6dca4984588a2799bfd8c474b9cbfdc46b7492dc560674824e50) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/d97c764cd40cdac514e24b00d93384752794dc9bb16efa172b807c8dea9dba38) #### sessions/sessionsList/SessionsList_AutomationsNewBadge_Narrow/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/04a7325a53906994ba4cd8544e3545a34f47d34ee217414edf6abad07d12a7d0) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/fd5859d2927a02ad5266bb6e2568aced68814032d3bac38790d122552d84a3b5) #### sessions/sessionsList/SessionsList_AutomationsNewBadge_Narrow/DarkHighContrast -![screenshot](https://hediet-screenshots.azurewebsites.net/images/f9ccc89121f0b10fb398a0ceb1f9302211dbb069e22cd6d13ede9f3411d4fcc9) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/e321f33fe07110b156e0b531e94e80c53d3f7049c763b0000a1173709797ee00) #### sessions/sessionsList/SessionsList_AutomationsNewBadge_Narrow/Light -![screenshot](https://hediet-screenshots.azurewebsites.net/images/b33a584913c10c11ccb45516c2d4977eb545a6a5f0d4505f39260927f2fe4cf9) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/2f190e02ac3855cc03db0c993311cd38eba505c5669b4aa5d5b9026d1e7a0403) #### sessions/sessionsList/SessionsList_AutomationsNewBadge_Running/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/ef6b6c9e71c37d7716ab8b0d404a2381377dc2fdbe55be09ac30b06ea0b6af9f) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/142ea1a930c5e18dc0c07448b2f532bfcb4be686d02793d82197c5a1058a369e) #### sessions/sessionsList/SessionsList_AutomationsNewBadge_Running/DarkHighContrast -![screenshot](https://hediet-screenshots.azurewebsites.net/images/b7852b606c3e4f28f354a58bfa342262c6800407b7494d8c6e1328452f1bb10a) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/29003b5f84a9ed3c26dabd5ab5b8cb07bcf3f13d1eefb0bd6155b9d734d7b297) #### sessions/sessionsList/SessionsList_AutomationsNewBadge_Running/Light -![screenshot](https://hediet-screenshots.azurewebsites.net/images/05183fa9e5bbe592f5d711d9661dfed3b6a8d7ea31f0d5c3b916787a8c954aa1) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/b1cbdec3839b92478a7aa184330d28a0eda0d21d21ed62b9eb613e1c5d82db99) #### sessions/sessionsList/SessionsList_AutomationsNewBadge_Soft/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/eec50d2a9a86dc1ec9f98e45cd23cdeb1a0c4c1057677ce352876cdeb94d75ed) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/b36e1cc379898bb27949013f04b4bc24908e3b6978e0a1b0a31561925dc2cd53) #### sessions/sessionsList/SessionsList_AutomationsNewBadge_Soft/DarkHighContrast -![screenshot](https://hediet-screenshots.azurewebsites.net/images/da60851cc3372baac8d29fd6ed651b39cc66347474b4c412788a255672484e87) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/8e2ae0a6ee82ff7b9229b6153b9999da0832c3c46cb9f6d706d3b44e668aebdd) #### sessions/sessionsList/SessionsList_AutomationsNewBadge_Soft/Light -![screenshot](https://hediet-screenshots.azurewebsites.net/images/af268bfa64dd8a47c8e05e756742156c27b0ff49306146e2aa343260b5838735) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/5426f06a850170368db6c9bd8fb423ecc7a7f9226ebba880e344ba0e807225cc) #### sessions/sessionsList/SessionsList_AutomationsNewBadge_Unread/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/83cd7defd6d83de4178701e9fd1c817f81f49ad06d2ae59760d958d4d6aa52bd) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/1c9f2d0c8c549f042b0c9e2c8166745d8150a1089ede2ac539d5c33e1196dfb9) #### sessions/sessionsList/SessionsList_AutomationsNewBadge_Unread/DarkHighContrast -![screenshot](https://hediet-screenshots.azurewebsites.net/images/a1db74bd19da0772ca4caeaf341646344e2cb775708c5732f00a30fd94473960) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/942f8917b5695c04f4f9ed3cb1fb064d34efb28710c788f2689c8b63a48700e4) #### sessions/sessionsList/SessionsList_AutomationsNewBadge_Unread/Light -![screenshot](https://hediet-screenshots.azurewebsites.net/images/38d000ecc104d74252f7bac7d47f712e8deb2bd4907657d50993cbf0ee9fcc3e) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/39db1002ce6b5806caf6691f1b3ede414309b03f4d845a959d5a64683279a789) #### sessions/sessionsList/SessionsList_AutomationsNewBadge/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/0ed9e3fcbd8e6a1b94016e7b8c9f823f5faad1e861814e3593a4a65373ddf6a7) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/cb56a044d6d3f4f3f941ccf532c48724d763014e3660c66090cc9bf150943504) #### sessions/sessionsList/SessionsList_AutomationsNewBadge/DarkHighContrast -![screenshot](https://hediet-screenshots.azurewebsites.net/images/2e624aeed24ef3d79e54a89d5e03ac4b2cd40ec4c7e35bdca430bc5729e36449) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/f6bbb99b9daf32fd92e5d5d9576936c6bb572f7661e14a2a47d0859d8885a1aa) #### sessions/sessionsList/SessionsList_AutomationsNewBadge/Light -![screenshot](https://hediet-screenshots.azurewebsites.net/images/0154b8cd8302a62959c0a067e02aadc24d27a54d85891dd19fb9e9b4d99362f2) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/18db8ef94c99dd9e879c03b4b5a584d0ecab76050405a9b2e91bacc484324314) #### sessions/sessionsList/SessionsList_CompactChatRename/Dark ![screenshot](https://hediet-screenshots.azurewebsites.net/images/1318fd99d4be3fbc1b84494549f0a4474d0d864e12430bd8b2cc4df99665fb52) @@ -490,22 +490,22 @@ ![screenshot](https://hediet-screenshots.azurewebsites.net/images/b58c784d3624334112da4034d215d88c03aac3c0919af6169d84b3ef5d70a979) #### sessions/sessionsList/SessionsList_LightweightNewButton/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/a061a58bb4fbabce4ce5badf826c4377a624fc9ba88dbc5ec66f0bfaaef9e3d4) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/0e7bd1a77f3f01449a15df6d7ba089d20d77abc05a302deaf2e253034ae30aab) #### sessions/sessionsList/SessionsList_LightweightNewButton/DarkHighContrast -![screenshot](https://hediet-screenshots.azurewebsites.net/images/50c9d63b886aa15e5aab690fca127c675b135ac76c12a36afa65cc40200e985b) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/6bdad7e78424074ac814e5dc07784d49924803f8c4f0e076e916344ad29ec1fb) #### sessions/sessionsList/SessionsList_LightweightNewButton/Light -![screenshot](https://hediet-screenshots.azurewebsites.net/images/27750194694cd6b4941e11c7752732c04f934269141b93ad9bbd6131217ace45) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/05eb58b0a29f99290491c32c30e230036c806f841f7dc0cbb318dea2076c4950) #### sessions/sessionsList/SessionsList_LightweightNewButtonWithKeybindingBackground/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/fb23f2caac47e86a717f4dfe4adb1b95c54c8b1afa3e25b712ece04403122043) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/6089d125a884489d8947bf730bdceb2938327a28096e3071a1777f896f3a5de3) #### sessions/sessionsList/SessionsList_LightweightNewButtonWithKeybindingBackground/DarkHighContrast -![screenshot](https://hediet-screenshots.azurewebsites.net/images/4ada330a003107db5221c84f4662aff80f1f4b541877e80420c0cf22918bf5b2) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/8ea355390ca61a6494b0b0f859fce663727dd595fc27caa68792483c7e358f1a) #### sessions/sessionsList/SessionsList_LightweightNewButtonWithKeybindingBackground/Light -![screenshot](https://hediet-screenshots.azurewebsites.net/images/54f5cf2d72ea720ff9c6f07388c686f91f7541abff62ef516b321588260da824) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/0b5412b078e26b869c1793bd120a1232cd773a7ff7c0e4f25c6eecf13ca8b9c3) #### sessions/sessionsList/SessionsList_NarrowHoverToolbar/Dark ![screenshot](https://hediet-screenshots.azurewebsites.net/images/5dae1fd2e220ae775ce867ca9407188c7670c0808ebaafa55ae0dbbc3809cccd)