mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-17 15:24:40 +01:00
components for AI Customization shortcuts widget
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { autorun } from '../../../../base/common/observable.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 { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { Button } from '../../../../base/browser/ui/button/button.js';
|
||||
import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js';
|
||||
import { IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';
|
||||
import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js';
|
||||
import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
|
||||
import { Menus } from '../../../browser/menus.js';
|
||||
import { getCustomizationTotalCount } from './customizationCounts.js';
|
||||
|
||||
const $ = DOM.$;
|
||||
|
||||
const CUSTOMIZATIONS_COLLAPSED_KEY = 'agentSessions.customizationsCollapsed';
|
||||
|
||||
export interface IAICustomizationShortcutsWidgetOptions {
|
||||
readonly onDidToggleCollapse?: () => void;
|
||||
}
|
||||
|
||||
export class AICustomizationShortcutsWidget extends Disposable {
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
options: IAICustomizationShortcutsWidgetOptions | undefined,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IPromptsService private readonly promptsService: IPromptsService,
|
||||
@IMcpService private readonly mcpService: IMcpService,
|
||||
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
|
||||
@IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._render(container, options);
|
||||
}
|
||||
|
||||
private _render(parent: HTMLElement, options: IAICustomizationShortcutsWidgetOptions | undefined): void {
|
||||
// Get initial collapsed state
|
||||
const isCollapsed = this.storageService.getBoolean(CUSTOMIZATIONS_COLLAPSED_KEY, StorageScope.PROFILE, false);
|
||||
|
||||
const container = DOM.append(parent, $('.ai-customization-toolbar'));
|
||||
if (isCollapsed) {
|
||||
container.classList.add('collapsed');
|
||||
}
|
||||
|
||||
// Header (clickable to toggle)
|
||||
const header = DOM.append(container, $('.ai-customization-header'));
|
||||
header.classList.toggle('collapsed', isCollapsed);
|
||||
|
||||
const headerButtonContainer = DOM.append(header, $('.customization-link-button-container'));
|
||||
const headerButton = this._register(new Button(headerButtonContainer, {
|
||||
...defaultButtonStyles,
|
||||
secondary: true,
|
||||
title: false,
|
||||
supportIcons: true,
|
||||
buttonSecondaryBackground: 'transparent',
|
||||
buttonSecondaryHoverBackground: undefined,
|
||||
buttonSecondaryForeground: undefined,
|
||||
buttonSecondaryBorder: undefined,
|
||||
}));
|
||||
headerButton.element.classList.add('customization-link-button', 'sidebar-action-button');
|
||||
headerButton.element.setAttribute('aria-expanded', String(!isCollapsed));
|
||||
headerButton.label = localize('customizations', "CUSTOMIZATIONS");
|
||||
|
||||
const chevronContainer = DOM.append(headerButton.element, $('span.customization-link-counts'));
|
||||
const chevron = DOM.append(chevronContainer, $('.ai-customization-chevron'));
|
||||
const headerTotalCount = DOM.append(chevronContainer, $('span.ai-customization-header-total.hidden'));
|
||||
chevron.classList.add(...ThemeIcon.asClassNameArray(isCollapsed ? Codicon.chevronRight : Codicon.chevronDown));
|
||||
|
||||
// Toolbar container
|
||||
const toolbarContainer = DOM.append(container, $('.ai-customization-toolbar-content.sidebar-action-list'));
|
||||
|
||||
this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, toolbarContainer, Menus.SidebarCustomizations, {
|
||||
hiddenItemStrategy: HiddenItemStrategy.NoHide,
|
||||
toolbarOptions: { primaryGroup: () => true },
|
||||
telemetrySource: 'sidebarCustomizations',
|
||||
}));
|
||||
|
||||
let updateCountRequestId = 0;
|
||||
const updateHeaderTotalCount = async () => {
|
||||
const requestId = ++updateCountRequestId;
|
||||
const totalCount = await getCustomizationTotalCount(this.promptsService, this.mcpService, this.workspaceService, this.workspaceContextService);
|
||||
if (requestId !== updateCountRequestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
headerTotalCount.classList.toggle('hidden', totalCount === 0);
|
||||
headerTotalCount.textContent = `${totalCount}`;
|
||||
};
|
||||
|
||||
this._register(this.promptsService.onDidChangeCustomAgents(() => updateHeaderTotalCount()));
|
||||
this._register(this.promptsService.onDidChangeSlashCommands(() => updateHeaderTotalCount()));
|
||||
this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(() => updateHeaderTotalCount()));
|
||||
this._register(autorun(reader => {
|
||||
this.mcpService.servers.read(reader);
|
||||
updateHeaderTotalCount();
|
||||
}));
|
||||
this._register(autorun(reader => {
|
||||
this.workspaceService.activeProjectRoot.read(reader);
|
||||
updateHeaderTotalCount();
|
||||
}));
|
||||
updateHeaderTotalCount();
|
||||
|
||||
// Toggle collapse on header click
|
||||
const transitionListener = this._register(new MutableDisposable());
|
||||
const toggleCollapse = () => {
|
||||
const collapsed = container.classList.toggle('collapsed');
|
||||
header.classList.toggle('collapsed', collapsed);
|
||||
this.storageService.store(CUSTOMIZATIONS_COLLAPSED_KEY, collapsed, StorageScope.PROFILE, StorageTarget.USER);
|
||||
headerButton.element.setAttribute('aria-expanded', String(!collapsed));
|
||||
chevron.classList.remove(...ThemeIcon.asClassNameArray(Codicon.chevronRight), ...ThemeIcon.asClassNameArray(Codicon.chevronDown));
|
||||
chevron.classList.add(...ThemeIcon.asClassNameArray(collapsed ? Codicon.chevronRight : Codicon.chevronDown));
|
||||
|
||||
// Re-layout after the transition
|
||||
transitionListener.value = DOM.addDisposableListener(toolbarContainer, 'transitionend', () => {
|
||||
transitionListener.clear();
|
||||
options?.onDidToggleCollapse?.();
|
||||
});
|
||||
};
|
||||
|
||||
this._register(headerButton.onDidClick(() => toggleCollapse()));
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ import { getSourceCounts, getSourceCountsTotal } from './customizationCounts.js'
|
||||
import { IEditorService, MODAL_GROUP } from '../../../../workbench/services/editor/common/editorService.js';
|
||||
import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
|
||||
|
||||
interface ICustomizationItemConfig {
|
||||
export interface ICustomizationItemConfig {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly icon: ThemeIcon;
|
||||
@@ -43,7 +43,7 @@ interface ICustomizationItemConfig {
|
||||
readonly isMcp?: boolean;
|
||||
}
|
||||
|
||||
const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [
|
||||
export const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [
|
||||
{
|
||||
id: 'sessions.customization.agents',
|
||||
label: localize('agents', "Agents"),
|
||||
@@ -92,7 +92,7 @@ const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [
|
||||
* Custom ActionViewItem for each customization link in the toolbar.
|
||||
* Renders icon + label + source count badges, matching the sidebar footer style.
|
||||
*/
|
||||
class CustomizationLinkViewItem extends ActionViewItem {
|
||||
export class CustomizationLinkViewItem extends ActionViewItem {
|
||||
|
||||
private readonly _viewItemDisposables: DisposableStore;
|
||||
private _button: Button | undefined;
|
||||
@@ -199,7 +199,7 @@ class CustomizationLinkViewItem extends ActionViewItem {
|
||||
|
||||
// --- Register actions and view items --- //
|
||||
|
||||
class CustomizationsToolbarContribution extends Disposable implements IWorkbenchContribution {
|
||||
export class CustomizationsToolbarContribution extends Disposable implements IWorkbenchContribution {
|
||||
|
||||
static readonly ID = 'workbench.contrib.sessionsCustomizationsToolbar';
|
||||
|
||||
|
||||
@@ -2,132 +2,129 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.agent-sessions-viewpane {
|
||||
|
||||
/* AI Customization section - pinned to bottom */
|
||||
.ai-customization-toolbar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
border-top: 1px solid var(--vscode-widget-border);
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
/* 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 - clickable for collapse */
|
||||
.ai-customization-toolbar .ai-customization-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ai-customization-toolbar .ai-customization-header:not(.collapsed) {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.ai-customization-toolbar .ai-customization-chevron {
|
||||
flex-shrink: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.ai-customization-toolbar .ai-customization-header:not(.collapsed) .customization-link-button:hover .ai-customization-chevron,
|
||||
.ai-customization-toolbar .ai-customization-header:not(.collapsed) .customization-link-button:focus-within .ai-customization-chevron,
|
||||
.ai-customization-toolbar .ai-customization-header.collapsed .customization-link-button:hover .ai-customization-chevron,
|
||||
.ai-customization-toolbar .ai-customization-header.collapsed .customization-link-button:focus-within .ai-customization-chevron {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.ai-customization-toolbar .ai-customization-header-total {
|
||||
display: none;
|
||||
opacity: 0.7;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.ai-customization-toolbar .ai-customization-header.collapsed .customization-link-button:not(:hover):not(:focus-within) .ai-customization-header-total:not(.hidden) {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.ai-customization-toolbar .ai-customization-header.collapsed .customization-link-button:hover .ai-customization-header-total,
|
||||
.ai-customization-toolbar .ai-customization-header.collapsed .customization-link-button:focus-within .ai-customization-header-total,
|
||||
.ai-customization-toolbar .ai-customization-header:not(.collapsed) .customization-link-button .ai-customization-header-total {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Button container - fills available space */
|
||||
.ai-customization-toolbar .customization-link-button-container {
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Button needs relative positioning for counts overlay */
|
||||
.ai-customization-toolbar .customization-link-button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Counts - floating right inside the button */
|
||||
.ai-customization-toolbar .customization-link-counts {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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: 12px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.ai-customization-toolbar .source-count-num {
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Collapsed state */
|
||||
.ai-customization-toolbar .ai-customization-toolbar-content {
|
||||
max-height: 500px;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.2s ease-out;
|
||||
}
|
||||
|
||||
.ai-customization-toolbar.collapsed .ai-customization-toolbar-content {
|
||||
max-height: 0;
|
||||
}
|
||||
/* AI Customization section - pinned to bottom */
|
||||
.ai-customization-toolbar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
border-top: 1px solid var(--vscode-widget-border);
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
/* 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 - clickable for collapse */
|
||||
.ai-customization-toolbar .ai-customization-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ai-customization-toolbar .ai-customization-header:not(.collapsed) {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.ai-customization-toolbar .ai-customization-chevron {
|
||||
flex-shrink: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.ai-customization-toolbar .ai-customization-header:not(.collapsed) .customization-link-button:hover .ai-customization-chevron,
|
||||
.ai-customization-toolbar .ai-customization-header:not(.collapsed) .customization-link-button:focus-within .ai-customization-chevron,
|
||||
.ai-customization-toolbar .ai-customization-header.collapsed .customization-link-button:hover .ai-customization-chevron,
|
||||
.ai-customization-toolbar .ai-customization-header.collapsed .customization-link-button:focus-within .ai-customization-chevron {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.ai-customization-toolbar .ai-customization-header-total {
|
||||
display: none;
|
||||
opacity: 0.7;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.ai-customization-toolbar .ai-customization-header.collapsed .customization-link-button:not(:hover):not(:focus-within) .ai-customization-header-total:not(.hidden) {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.ai-customization-toolbar .ai-customization-header.collapsed .customization-link-button:hover .ai-customization-header-total,
|
||||
.ai-customization-toolbar .ai-customization-header.collapsed .customization-link-button:focus-within .ai-customization-header-total,
|
||||
.ai-customization-toolbar .ai-customization-header:not(.collapsed) .customization-link-button .ai-customization-header-total {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Button container - fills available space */
|
||||
.ai-customization-toolbar .customization-link-button-container {
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Button needs relative positioning for counts overlay */
|
||||
.ai-customization-toolbar .customization-link-button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Counts - floating right inside the button */
|
||||
.ai-customization-toolbar .customization-link-counts {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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: 12px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.ai-customization-toolbar .source-count-num {
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Collapsed state */
|
||||
.ai-customization-toolbar .ai-customization-toolbar-content {
|
||||
max-height: 500px;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.2s ease-out;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.ai-customization-toolbar.collapsed .ai-customization-toolbar-content {
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
@@ -3,15 +3,11 @@
|
||||
* 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 './media/sessionsViewPane.css';
|
||||
import * as DOM from '../../../../base/browser/dom.js';
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
|
||||
import { MutableDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { autorun } from '../../../../base/common/observable.js';
|
||||
import { ThemeIcon } from '../../../../base/common/themables.js';
|
||||
import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { EditorsVisibleContext } from '../../../../workbench/common/contextkeys.js';
|
||||
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
|
||||
@@ -29,9 +25,6 @@ import { localize, localize2 } from '../../../../nls.js';
|
||||
import { AgentSessionsControl } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsControl.js';
|
||||
import { AgentSessionsFilter, AgentSessionsGrouping } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.js';
|
||||
import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js';
|
||||
import { IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';
|
||||
import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js';
|
||||
import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
|
||||
import { ISessionsManagementService, IsNewChatSessionContext } from './sessionsManagementService.js';
|
||||
import { Action2, ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js';
|
||||
@@ -40,26 +33,19 @@ import { Button } from '../../../../base/browser/ui/button/button.js';
|
||||
import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js';
|
||||
import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { ACTION_ID_NEW_CHAT } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js';
|
||||
import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';
|
||||
import { Menus } from '../../../browser/menus.js';
|
||||
import { getCustomizationTotalCount } from './customizationCounts.js';
|
||||
import { AICustomizationShortcutsWidget } from './aiCustomizationShortcutsWidget.js';
|
||||
import { IHostService } from '../../../../workbench/services/host/browser/host.js';
|
||||
|
||||
const $ = DOM.$;
|
||||
export const SessionsViewId = 'agentic.workbench.view.sessionsView';
|
||||
const SessionsViewFilterSubMenu = new MenuId('AgentSessionsViewFilterSubMenu');
|
||||
|
||||
const CUSTOMIZATIONS_COLLAPSED_KEY = 'agentSessions.customizationsCollapsed';
|
||||
|
||||
export class AgenticSessionsViewPane extends ViewPane {
|
||||
|
||||
private viewPaneContainer: HTMLElement | undefined;
|
||||
private sessionsControlContainer: HTMLElement | undefined;
|
||||
sessionsControl: AgentSessionsControl | undefined;
|
||||
private aiCustomizationContainer: HTMLElement | undefined;
|
||||
|
||||
constructor(
|
||||
options: IViewPaneOptions,
|
||||
@@ -73,13 +59,8 @@ export class AgenticSessionsViewPane extends ViewPane {
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IHoverService hoverService: IHoverService,
|
||||
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IPromptsService private readonly promptsService: IPromptsService,
|
||||
@IMcpService private readonly mcpService: IMcpService,
|
||||
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
|
||||
@ISessionsManagementService private readonly activeSessionService: ISessionsManagementService,
|
||||
@IHostService private readonly hostService: IHostService,
|
||||
@IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService,
|
||||
) {
|
||||
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
|
||||
}
|
||||
@@ -180,8 +161,14 @@ export class AgenticSessionsViewPane extends ViewPane {
|
||||
}));
|
||||
|
||||
// AI Customization toolbar (bottom, fixed height)
|
||||
this.aiCustomizationContainer = DOM.append(sessionsContainer, $('div'));
|
||||
this.createAICustomizationShortcuts(this.aiCustomizationContainer);
|
||||
this._register(this.instantiationService.createInstance(AICustomizationShortcutsWidget, sessionsContainer, {
|
||||
onDidToggleCollapse: () => {
|
||||
if (this.viewPaneContainer) {
|
||||
const { offsetHeight, offsetWidth } = this.viewPaneContainer;
|
||||
this.layoutBody(offsetHeight, offsetWidth);
|
||||
}
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
private restoreLastSelectedSession(): void {
|
||||
@@ -191,96 +178,6 @@ export class AgenticSessionsViewPane extends ViewPane {
|
||||
}
|
||||
}
|
||||
|
||||
private createAICustomizationShortcuts(container: HTMLElement): void {
|
||||
// Get initial collapsed state
|
||||
const isCollapsed = this.storageService.getBoolean(CUSTOMIZATIONS_COLLAPSED_KEY, StorageScope.PROFILE, false);
|
||||
|
||||
container.classList.add('ai-customization-toolbar');
|
||||
if (isCollapsed) {
|
||||
container.classList.add('collapsed');
|
||||
}
|
||||
|
||||
// Header (clickable to toggle)
|
||||
const header = DOM.append(container, $('.ai-customization-header'));
|
||||
header.classList.toggle('collapsed', isCollapsed);
|
||||
|
||||
const headerButtonContainer = DOM.append(header, $('.customization-link-button-container'));
|
||||
const headerButton = this._register(new Button(headerButtonContainer, {
|
||||
...defaultButtonStyles,
|
||||
secondary: true,
|
||||
title: false,
|
||||
supportIcons: true,
|
||||
buttonSecondaryBackground: 'transparent',
|
||||
buttonSecondaryHoverBackground: undefined,
|
||||
buttonSecondaryForeground: undefined,
|
||||
buttonSecondaryBorder: undefined,
|
||||
}));
|
||||
headerButton.element.classList.add('customization-link-button', 'sidebar-action-button');
|
||||
headerButton.element.setAttribute('aria-expanded', String(!isCollapsed));
|
||||
headerButton.label = localize('customizations', "CUSTOMIZATIONS");
|
||||
|
||||
const chevronContainer = DOM.append(headerButton.element, $('span.customization-link-counts'));
|
||||
const chevron = DOM.append(chevronContainer, $('.ai-customization-chevron'));
|
||||
const headerTotalCount = DOM.append(chevronContainer, $('span.ai-customization-header-total.hidden'));
|
||||
chevron.classList.add(...ThemeIcon.asClassNameArray(isCollapsed ? Codicon.chevronRight : Codicon.chevronDown));
|
||||
|
||||
// Toolbar container
|
||||
const toolbarContainer = DOM.append(container, $('.ai-customization-toolbar-content.sidebar-action-list'));
|
||||
|
||||
this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, toolbarContainer, Menus.SidebarCustomizations, {
|
||||
hiddenItemStrategy: HiddenItemStrategy.NoHide,
|
||||
toolbarOptions: { primaryGroup: () => true },
|
||||
telemetrySource: 'sidebarCustomizations',
|
||||
}));
|
||||
|
||||
let updateCountRequestId = 0;
|
||||
const updateHeaderTotalCount = async () => {
|
||||
const requestId = ++updateCountRequestId;
|
||||
const totalCount = await getCustomizationTotalCount(this.promptsService, this.mcpService, this.workspaceService, this.workspaceContextService);
|
||||
if (requestId !== updateCountRequestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
headerTotalCount.classList.toggle('hidden', totalCount === 0);
|
||||
headerTotalCount.textContent = `${totalCount}`;
|
||||
};
|
||||
|
||||
this._register(this.promptsService.onDidChangeCustomAgents(() => updateHeaderTotalCount()));
|
||||
this._register(this.promptsService.onDidChangeSlashCommands(() => updateHeaderTotalCount()));
|
||||
this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(() => updateHeaderTotalCount()));
|
||||
this._register(autorun(reader => {
|
||||
this.mcpService.servers.read(reader);
|
||||
updateHeaderTotalCount();
|
||||
}));
|
||||
this._register(autorun(reader => {
|
||||
this.workspaceService.activeProjectRoot.read(reader);
|
||||
updateHeaderTotalCount();
|
||||
}));
|
||||
updateHeaderTotalCount();
|
||||
|
||||
// Toggle collapse on header click
|
||||
const transitionListener = this._register(new MutableDisposable());
|
||||
const toggleCollapse = () => {
|
||||
const collapsed = container.classList.toggle('collapsed');
|
||||
header.classList.toggle('collapsed', collapsed);
|
||||
this.storageService.store(CUSTOMIZATIONS_COLLAPSED_KEY, collapsed, StorageScope.PROFILE, StorageTarget.USER);
|
||||
headerButton.element.setAttribute('aria-expanded', String(!collapsed));
|
||||
chevron.classList.remove(...ThemeIcon.asClassNameArray(Codicon.chevronRight), ...ThemeIcon.asClassNameArray(Codicon.chevronDown));
|
||||
chevron.classList.add(...ThemeIcon.asClassNameArray(collapsed ? Codicon.chevronRight : Codicon.chevronDown));
|
||||
|
||||
// Re-layout after the transition so sessions control gets the right height
|
||||
transitionListener.value = DOM.addDisposableListener(toolbarContainer, 'transitionend', () => {
|
||||
transitionListener.clear();
|
||||
if (this.viewPaneContainer) {
|
||||
const { offsetHeight, offsetWidth } = this.viewPaneContainer;
|
||||
this.layoutBody(offsetHeight, offsetWidth);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this._register(headerButton.onDidClick(() => toggleCollapse()));
|
||||
}
|
||||
|
||||
private getSessionHoverPosition(): HoverPosition {
|
||||
const viewLocation = this.viewDescriptorService.getViewLocationById(this.id);
|
||||
const sideBarPosition = this.layoutService.getSideBarPosition();
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { observableValue } from '../../../../../base/common/observable.js';
|
||||
import { URI } from '../../../../../base/common/uri.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 { IStorageService, StorageScope } from '../../../../../platform/storage/common/storage.js';
|
||||
import { IFileService } from '../../../../../platform/files/common/files.js';
|
||||
import { IWorkspace, IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
|
||||
import { IPromptsService, PromptsStorage } from '../../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';
|
||||
import { PromptsType } from '../../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js';
|
||||
import { ILanguageModelsService } from '../../../../../workbench/contrib/chat/common/languageModels.js';
|
||||
import { IMcpServer, IMcpService } from '../../../../../workbench/contrib/mcp/common/mcpTypes.js';
|
||||
import { IAICustomizationWorkspaceService, IStorageSourceFilter } from '../../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
|
||||
import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from '../../../../../workbench/test/browser/componentFixtures/fixtureUtils.js';
|
||||
import { AICustomizationShortcutsWidget } from '../../browser/aiCustomizationShortcutsWidget.js';
|
||||
import { CUSTOMIZATION_ITEMS, CustomizationLinkViewItem } from '../../browser/customizationsToolbar.contribution.js';
|
||||
import { IActiveSessionItem, ISessionsManagementService } from '../../browser/sessionsManagementService.js';
|
||||
import { Menus } from '../../../../browser/menus.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();
|
||||
for (const [index, config] of CUSTOMIZATION_ITEMS.entries()) {
|
||||
menuRegistrations.add(MenuRegistry.appendMenuItem(Menus.SidebarCustomizations, {
|
||||
command: { id: config.id, title: config.label },
|
||||
group: 'navigation',
|
||||
order: index + 1,
|
||||
}));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FixtureMenuService — reads from MenuRegistry without context-key filtering
|
||||
// (MockContextKeyService.contextMatchesRules always returns false, which hides
|
||||
// every item when using the real MenuService.)
|
||||
// ============================================================================
|
||||
|
||||
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<string>(); }
|
||||
resetHiddenStates() { }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Minimal IActionViewItemService that supports register/lookUp
|
||||
// ============================================================================
|
||||
|
||||
class FixtureActionViewItemService implements IActionViewItemService {
|
||||
declare _serviceBrand: undefined;
|
||||
|
||||
private readonly _providers = new Map<string, IActionViewItemFactory>();
|
||||
private readonly _onDidChange = new Emitter<MenuId>();
|
||||
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 helpers
|
||||
// ============================================================================
|
||||
|
||||
const defaultFilter: IStorageSourceFilter = {
|
||||
sources: [PromptsStorage.local, PromptsStorage.user, PromptsStorage.extension],
|
||||
};
|
||||
|
||||
function createMockPromptsService(): IPromptsService {
|
||||
return createMockPromptsServiceWithCounts();
|
||||
}
|
||||
|
||||
interface ICustomizationCounts {
|
||||
readonly agents?: number;
|
||||
readonly skills?: number;
|
||||
readonly instructions?: number;
|
||||
readonly prompts?: number;
|
||||
readonly hooks?: number;
|
||||
}
|
||||
|
||||
function createMockPromptsServiceWithCounts(counts?: ICustomizationCounts): IPromptsService {
|
||||
const fakeUri = (prefix: string, i: number) => URI.parse(`file:///mock/${prefix}-${i}.md`);
|
||||
const fakeItem = (prefix: string, i: number) => ({ uri: fakeUri(prefix, i), storage: PromptsStorage.local });
|
||||
|
||||
const agents = Array.from({ length: counts?.agents ?? 0 }, (_, i) => ({
|
||||
uri: fakeUri('agent', i),
|
||||
source: { storage: PromptsStorage.local },
|
||||
}));
|
||||
const skills = Array.from({ length: counts?.skills ?? 0 }, (_, i) => fakeItem('skill', i));
|
||||
const prompts = Array.from({ length: counts?.prompts ?? 0 }, (_, i) => ({
|
||||
promptPath: { uri: fakeUri('prompt', i), storage: PromptsStorage.local, type: PromptsType.prompt },
|
||||
}));
|
||||
const instructions = Array.from({ length: counts?.instructions ?? 0 }, (_, i) => fakeItem('instructions', i));
|
||||
const hooks = Array.from({ length: counts?.hooks ?? 0 }, (_, i) => fakeItem('hook', i));
|
||||
|
||||
return new class extends mock<IPromptsService>() {
|
||||
override readonly onDidChangeCustomAgents = Event.None;
|
||||
override readonly onDidChangeSlashCommands = Event.None;
|
||||
override async getCustomAgents() { return agents as never[]; }
|
||||
override async findAgentSkills() { return skills as never[]; }
|
||||
override async getPromptSlashCommands() { return prompts as never[]; }
|
||||
override async listPromptFiles(type: PromptsType) {
|
||||
return (type === PromptsType.hook ? hooks : instructions) as never[];
|
||||
}
|
||||
override async listAgentInstructions() { return [] as never[]; }
|
||||
}();
|
||||
}
|
||||
|
||||
function createMockMcpService(serverCount: number = 0): IMcpService {
|
||||
const MockServer = mock<IMcpServer>();
|
||||
const servers = observableValue<readonly IMcpServer[]>('mockMcpServers', Array.from({ length: serverCount }, () => new MockServer()));
|
||||
return new class extends mock<IMcpService>() {
|
||||
override readonly servers = servers;
|
||||
}();
|
||||
}
|
||||
|
||||
function createMockWorkspaceService(): IAICustomizationWorkspaceService {
|
||||
const activeProjectRoot = observableValue<URI | undefined>('mockActiveProjectRoot', undefined);
|
||||
return new class extends mock<IAICustomizationWorkspaceService>() {
|
||||
override readonly activeProjectRoot = activeProjectRoot;
|
||||
override getActiveProjectRoot() { return undefined; }
|
||||
override getStorageSourceFilter() { return defaultFilter; }
|
||||
}();
|
||||
}
|
||||
|
||||
function createMockWorkspaceContextService(): IWorkspaceContextService {
|
||||
return new class extends mock<IWorkspaceContextService>() {
|
||||
override readonly onDidChangeWorkspaceFolders = Event.None;
|
||||
override getWorkspace(): IWorkspace { return { id: 'test', folders: [] }; }
|
||||
}();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Render helper
|
||||
// ============================================================================
|
||||
|
||||
function renderWidget(ctx: ComponentFixtureContext, options?: { mcpServerCount?: number; collapsed?: boolean; counts?: ICustomizationCounts }): void {
|
||||
ctx.container.style.width = '300px';
|
||||
ctx.container.style.backgroundColor = 'var(--vscode-sideBar-background)';
|
||||
|
||||
const actionViewItemService = new FixtureActionViewItemService();
|
||||
|
||||
const instantiationService = createEditorServices(ctx.disposableStore, {
|
||||
colorTheme: ctx.theme,
|
||||
additionalServices: (reg) => {
|
||||
// Register overrides BEFORE registerWorkbenchServices so they take priority
|
||||
reg.defineInstance(IMenuService, new FixtureMenuService());
|
||||
reg.defineInstance(IActionViewItemService, actionViewItemService);
|
||||
registerWorkbenchServices(reg);
|
||||
// Services needed by AICustomizationShortcutsWidget
|
||||
reg.defineInstance(IPromptsService, options?.counts ? createMockPromptsServiceWithCounts(options.counts) : createMockPromptsService());
|
||||
reg.defineInstance(IMcpService, createMockMcpService(options?.mcpServerCount ?? 0));
|
||||
reg.defineInstance(IAICustomizationWorkspaceService, createMockWorkspaceService());
|
||||
reg.defineInstance(IWorkspaceContextService, createMockWorkspaceContextService());
|
||||
// Additional services needed by CustomizationLinkViewItem
|
||||
reg.defineInstance(ILanguageModelsService, new class extends mock<ILanguageModelsService>() {
|
||||
override readonly onDidChangeLanguageModels = Event.None;
|
||||
}());
|
||||
reg.defineInstance(ISessionsManagementService, new class extends mock<ISessionsManagementService>() {
|
||||
override readonly activeSession = observableValue<IActiveSessionItem | undefined>('activeSession', undefined);
|
||||
}());
|
||||
reg.defineInstance(IFileService, new class extends mock<IFileService>() {
|
||||
override readonly onDidFilesChange = Event.None;
|
||||
}());
|
||||
},
|
||||
});
|
||||
|
||||
// Register view item factories from the real CustomizationLinkViewItem (per-render, instance-scoped)
|
||||
for (const config of CUSTOMIZATION_ITEMS) {
|
||||
ctx.disposableStore.add(actionViewItemService.register(Menus.SidebarCustomizations, config.id, (action, options) => {
|
||||
return instantiationService.createInstance(CustomizationLinkViewItem, action, options, config);
|
||||
}));
|
||||
}
|
||||
|
||||
// Override storage to set initial collapsed state
|
||||
if (options?.collapsed) {
|
||||
const storageService = instantiationService.get(IStorageService);
|
||||
instantiationService.set(IStorageService, new class extends mock<IStorageService>() {
|
||||
override getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean) {
|
||||
if (key === 'agentSessions.customizationsCollapsed') {
|
||||
return true;
|
||||
}
|
||||
return storageService.getBoolean(key, scope, fallbackValue!);
|
||||
}
|
||||
override store() { }
|
||||
}());
|
||||
}
|
||||
|
||||
// Create the widget (uses FixtureMenuService → reads MenuRegistry items registered above)
|
||||
ctx.disposableStore.add(
|
||||
instantiationService.createInstance(AICustomizationShortcutsWidget, ctx.container, undefined)
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Fixtures
|
||||
// ============================================================================
|
||||
|
||||
export default defineThemedFixtureGroup({ path: 'sessions/' }, {
|
||||
|
||||
Expanded: defineComponentFixture({
|
||||
labels: { kind: 'screenshot' },
|
||||
render: (ctx) => renderWidget(ctx),
|
||||
}),
|
||||
|
||||
Collapsed: defineComponentFixture({
|
||||
labels: { kind: 'screenshot' },
|
||||
render: (ctx) => renderWidget(ctx, { collapsed: true }),
|
||||
}),
|
||||
|
||||
WithMcpServers: defineComponentFixture({
|
||||
labels: { kind: 'screenshot' },
|
||||
render: (ctx) => renderWidget(ctx, { mcpServerCount: 3 }),
|
||||
}),
|
||||
|
||||
CollapsedWithMcpServers: defineComponentFixture({
|
||||
labels: { kind: 'screenshot' },
|
||||
render: (ctx) => renderWidget(ctx, { mcpServerCount: 3, collapsed: true }),
|
||||
}),
|
||||
|
||||
WithCounts: defineComponentFixture({
|
||||
labels: { kind: 'screenshot' },
|
||||
render: (ctx) => renderWidget(ctx, {
|
||||
mcpServerCount: 2,
|
||||
counts: { agents: 2, skills: 30, instructions: 16, prompts: 17, hooks: 4 },
|
||||
}),
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user