mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-15 04:41:00 +01:00
Merge pull request #296059 from microsoft/benibenj/chilly-octopus
Fix styling and functionality of sidebar sessions
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.sidebar-action-list .actions-container {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.sidebar-action > .action-label {
|
||||
/* Hide the default action-label rendered by ActionViewItem */
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Shared styling for interactive sidebar action buttons (account widget, customization links, etc.) */
|
||||
.sidebar-action-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
height: auto;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background: transparent;
|
||||
color: var(--vscode-sideBar-foreground);
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
justify-content: flex-start;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.sidebar-action-button:hover {
|
||||
background-color: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.sidebar-action-button.monaco-text-button:focus {
|
||||
outline-offset: -1px !important;
|
||||
}
|
||||
|
||||
.sidebar-action-button.monaco-text-button .codicon {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -31,18 +31,27 @@
|
||||
/* Sidebar Footer Container */
|
||||
.monaco-workbench .part.sidebar > .sidebar-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
gap: 4px;
|
||||
padding: 6px;
|
||||
border-top: 1px solid var(--vscode-sideBarSectionHeader-border, transparent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Make the toolbar and its action-item fill the full footer width */
|
||||
/* Make the toolbar fill the footer width and stack actions vertically */
|
||||
.monaco-workbench .part.sidebar > .sidebar-footer .monaco-toolbar,
|
||||
.monaco-workbench .part.sidebar > .sidebar-footer .monaco-action-bar,
|
||||
.monaco-workbench .part.sidebar > .sidebar-footer .actions-container,
|
||||
|
||||
.monaco-workbench .part.sidebar > .sidebar-footer .actions-container {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.sidebar > .sidebar-footer .action-item {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
cursor: default;
|
||||
|
||||
@@ -53,8 +53,13 @@ export class SidebarPart extends AbstractPaneCompositePart {
|
||||
static readonly MARGIN_TOP = 0;
|
||||
static readonly MARGIN_BOTTOM = 0;
|
||||
static readonly MARGIN_LEFT = 0;
|
||||
static readonly FOOTER_HEIGHT = 39;
|
||||
private static readonly FOOTER_ITEM_HEIGHT = 26;
|
||||
private static readonly FOOTER_ITEM_GAP = 4;
|
||||
private static readonly FOOTER_VERTICAL_PADDING = 6;
|
||||
|
||||
private footerContainer: HTMLElement | undefined;
|
||||
private footerToolbar: MenuWorkbenchToolBar | undefined;
|
||||
private previousLayoutDimensions: { width: number; height: number; top: number; left: number } | undefined;
|
||||
|
||||
//#region IView
|
||||
|
||||
@@ -167,13 +172,41 @@ export class SidebarPart extends AbstractPaneCompositePart {
|
||||
}
|
||||
|
||||
private createFooter(parent: HTMLElement): void {
|
||||
const footer = append(parent, $('.sidebar-footer'));
|
||||
const footer = append(parent, $('.sidebar-footer.sidebar-action-list'));
|
||||
this.footerContainer = footer;
|
||||
|
||||
this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, footer, Menus.SidebarFooter, {
|
||||
this.footerToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, footer, Menus.SidebarFooter, {
|
||||
hiddenItemStrategy: HiddenItemStrategy.NoHide,
|
||||
toolbarOptions: { primaryGroup: () => true },
|
||||
telemetrySource: 'sidebarFooter',
|
||||
}));
|
||||
|
||||
this._register(this.footerToolbar.onDidChangeMenuItems(() => {
|
||||
if (this.previousLayoutDimensions) {
|
||||
const { width, height, top, left } = this.previousLayoutDimensions;
|
||||
this.layout(width, height, top, left);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private getFooterHeight(): number {
|
||||
const actionCount = this.footerToolbar?.getItemsLength() ?? 0;
|
||||
if (actionCount === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return SidebarPart.FOOTER_VERTICAL_PADDING * 2
|
||||
+ (actionCount * SidebarPart.FOOTER_ITEM_HEIGHT)
|
||||
+ ((actionCount - 1) * SidebarPart.FOOTER_ITEM_GAP);
|
||||
}
|
||||
|
||||
private updateFooterVisibility(): void {
|
||||
const footer = this.footerContainer;
|
||||
if (!footer) {
|
||||
return;
|
||||
}
|
||||
|
||||
footer.style.display = this.getFooterHeight() > 0 ? '' : 'none';
|
||||
}
|
||||
|
||||
override updateStyles(): void {
|
||||
@@ -193,14 +226,19 @@ export class SidebarPart extends AbstractPaneCompositePart {
|
||||
}
|
||||
|
||||
override layout(width: number, height: number, top: number, left: number): void {
|
||||
this.previousLayoutDimensions = { width, height, top, left };
|
||||
|
||||
if (!this.layoutService.isVisible(Parts.SIDEBAR_PART)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateFooterVisibility();
|
||||
const footerHeight = Math.min(height, this.getFooterHeight());
|
||||
|
||||
// Layout content with reduced height to account for footer
|
||||
super.layout(
|
||||
width,
|
||||
height - SidebarPart.FOOTER_HEIGHT,
|
||||
height - footerHeight,
|
||||
top, left
|
||||
);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import '../../../browser/media/sidebarActionButton.css';
|
||||
import './media/accountWidget.css';
|
||||
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
|
||||
import { localize, localize2 } from '../../../../nls.js';
|
||||
@@ -11,7 +12,7 @@ import { ContextKeyExpr, IContextKeyService } from '../../../../platform/context
|
||||
import { IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js';
|
||||
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js';
|
||||
import { appendUpdateMenuItems as registerUpdateMenuItems } from '../../../../workbench/contrib/update/browser/update.js';
|
||||
import { appendUpdateMenuItems as registerUpdateMenuItems, CONTEXT_UPDATE_STATE } from '../../../../workbench/contrib/update/browser/update.js';
|
||||
import { Menus } from '../../../browser/menus.js';
|
||||
import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js';
|
||||
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
|
||||
@@ -84,7 +85,6 @@ registerUpdateMenuItems(AccountMenu, '3_updates');
|
||||
class AccountWidget extends ActionViewItem {
|
||||
|
||||
private accountButton: Button | undefined;
|
||||
private updateButton: Button | undefined;
|
||||
private readonly viewItemDisposables = this._register(new DisposableStore());
|
||||
|
||||
constructor(
|
||||
@@ -94,14 +94,17 @@ class AccountWidget extends ActionViewItem {
|
||||
@IContextMenuService private readonly contextMenuService: IContextMenuService,
|
||||
@IMenuService private readonly menuService: IMenuService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@IUpdateService private readonly updateService: IUpdateService,
|
||||
) {
|
||||
super(undefined, action, { ...options, icon: false, label: false });
|
||||
}
|
||||
|
||||
protected override getTooltip(): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
override render(container: HTMLElement): void {
|
||||
super.render(container);
|
||||
container.classList.add('account-widget');
|
||||
container.classList.add('account-widget', 'sidebar-action');
|
||||
|
||||
// Account button (left)
|
||||
const accountContainer = append(container, $('.account-widget-account'));
|
||||
@@ -115,7 +118,7 @@ class AccountWidget extends ActionViewItem {
|
||||
buttonSecondaryForeground: undefined,
|
||||
buttonSecondaryBorder: undefined,
|
||||
}));
|
||||
this.accountButton.element.classList.add('account-widget-account-button');
|
||||
this.accountButton.element.classList.add('account-widget-account-button', 'sidebar-action-button');
|
||||
|
||||
this.updateAccountButton();
|
||||
this.viewItemDisposables.add(this.defaultAccountService.onDidChangeDefaultAccount(() => this.updateAccountButton()));
|
||||
@@ -125,38 +128,6 @@ class AccountWidget extends ActionViewItem {
|
||||
e?.stopPropagation();
|
||||
this.showAccountMenu(this.accountButton!.element);
|
||||
}));
|
||||
|
||||
// Update button (shown for progress and restart-to-update states)
|
||||
const updateContainer = append(container, $('.account-widget-update'));
|
||||
this.updateButton = this.viewItemDisposables.add(new Button(updateContainer, {
|
||||
...defaultButtonStyles,
|
||||
secondary: true,
|
||||
title: false,
|
||||
supportIcons: true,
|
||||
buttonSecondaryBackground: 'transparent',
|
||||
buttonSecondaryHoverBackground: undefined,
|
||||
buttonSecondaryForeground: undefined,
|
||||
buttonSecondaryBorder: undefined,
|
||||
}));
|
||||
this.updateButton.element.classList.add('account-widget-update-button');
|
||||
this.updateButton.label = `$(${Codicon.debugRestart.id}) ${localize('update', "Update")}`;
|
||||
this.viewItemDisposables.add(this.updateButton.onDidClick(() => this.update()));
|
||||
|
||||
this.updateUpdateButton();
|
||||
this.viewItemDisposables.add(this.updateService.onStateChange(() => this.updateUpdateButton()));
|
||||
}
|
||||
|
||||
private isUpdateAvailable(): boolean {
|
||||
return this.updateService.state.type === StateType.Ready;
|
||||
}
|
||||
|
||||
private isUpdateInProgress(): boolean {
|
||||
const type = this.updateService.state.type;
|
||||
return type === StateType.CheckingForUpdates
|
||||
|| type === StateType.Downloading
|
||||
|| type === StateType.Downloaded
|
||||
|| type === StateType.Updating
|
||||
|| type === StateType.Overwriting;
|
||||
}
|
||||
|
||||
private showAccountMenu(anchor: HTMLElement): void {
|
||||
@@ -165,21 +136,12 @@ class AccountWidget extends ActionViewItem {
|
||||
fillInActionBarActions(menu.getActions(), actions);
|
||||
menu.dispose();
|
||||
|
||||
if (this.isUpdateAvailable()) {
|
||||
// Update button visible: open above the button
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => anchor,
|
||||
getActions: () => actions,
|
||||
anchorAlignment: AnchorAlignment.LEFT,
|
||||
});
|
||||
} else {
|
||||
// No update button: open to the right of the button
|
||||
const rect = anchor.getBoundingClientRect();
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => ({ x: rect.right, y: rect.top }),
|
||||
getActions: () => actions,
|
||||
});
|
||||
}
|
||||
const rect = anchor.getBoundingClientRect();
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => ({ x: rect.right, y: rect.top }),
|
||||
getActions: () => actions,
|
||||
anchorAlignment: AnchorAlignment.LEFT,
|
||||
});
|
||||
}
|
||||
|
||||
private async updateAccountButton(): Promise<void> {
|
||||
@@ -195,22 +157,77 @@ class AccountWidget extends ActionViewItem {
|
||||
: `$(${Codicon.account.id}) ${localize('signInLabel', "Sign In")}`;
|
||||
}
|
||||
|
||||
|
||||
override onClick(): void {
|
||||
// Handled by custom click handlers
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateWidget extends ActionViewItem {
|
||||
|
||||
private updateButton: Button | undefined;
|
||||
private readonly viewItemDisposables = this._register(new DisposableStore());
|
||||
|
||||
constructor(
|
||||
action: IAction,
|
||||
options: IBaseActionViewItemOptions,
|
||||
@IUpdateService private readonly updateService: IUpdateService,
|
||||
) {
|
||||
super(undefined, action, { ...options, icon: false, label: false });
|
||||
}
|
||||
|
||||
protected override getTooltip(): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
override render(container: HTMLElement): void {
|
||||
super.render(container);
|
||||
container.classList.add('update-widget', 'sidebar-action');
|
||||
|
||||
const updateContainer = append(container, $('.update-widget-action'));
|
||||
this.updateButton = this.viewItemDisposables.add(new Button(updateContainer, {
|
||||
...defaultButtonStyles,
|
||||
secondary: true,
|
||||
title: false,
|
||||
supportIcons: true,
|
||||
buttonSecondaryBackground: 'transparent',
|
||||
buttonSecondaryHoverBackground: undefined,
|
||||
buttonSecondaryForeground: undefined,
|
||||
buttonSecondaryBorder: undefined,
|
||||
}));
|
||||
this.updateButton.element.classList.add('update-widget-button', 'sidebar-action-button');
|
||||
this.viewItemDisposables.add(this.updateButton.onDidClick(() => this.update()));
|
||||
|
||||
this.updateUpdateButton();
|
||||
this.viewItemDisposables.add(this.updateService.onStateChange(() => this.updateUpdateButton()));
|
||||
}
|
||||
|
||||
private isUpdateReady(): boolean {
|
||||
return this.updateService.state.type === StateType.Ready;
|
||||
}
|
||||
|
||||
private isUpdatePending(): boolean {
|
||||
const type = this.updateService.state.type;
|
||||
return type === StateType.AvailableForDownload
|
||||
|| type === StateType.CheckingForUpdates
|
||||
|| type === StateType.Downloading
|
||||
|| type === StateType.Downloaded
|
||||
|| type === StateType.Updating
|
||||
|| type === StateType.Overwriting;
|
||||
}
|
||||
|
||||
private updateUpdateButton(): void {
|
||||
if (!this.updateButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = this.updateService.state;
|
||||
if (this.isUpdateInProgress()) {
|
||||
this.updateButton.element.parentElement!.style.display = '';
|
||||
if (this.isUpdatePending() && !this.isUpdateReady()) {
|
||||
this.updateButton.enabled = false;
|
||||
this.updateButton.label = `$(${Codicon.loading.id}~spin) ${this.getUpdateProgressMessage(state.type)}`;
|
||||
} else if (this.isUpdateAvailable()) {
|
||||
this.updateButton.element.parentElement!.style.display = '';
|
||||
} else {
|
||||
this.updateButton.enabled = true;
|
||||
this.updateButton.label = `$(${Codicon.debugRestart.id}) ${localize('update', "Update")}`;
|
||||
} else {
|
||||
this.updateButton.element.parentElement!.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,6 +274,11 @@ class AccountWidgetContribution extends Disposable implements IWorkbenchContribu
|
||||
return instantiationService.createInstance(AccountWidget, action, options);
|
||||
}, undefined));
|
||||
|
||||
const sessionsUpdateWidgetAction = 'sessions.action.updateWidget';
|
||||
this._register(actionViewItemService.register(Menus.SidebarFooter, sessionsUpdateWidgetAction, (action, options) => {
|
||||
return instantiationService.createInstance(UpdateWidget, action, options);
|
||||
}, undefined));
|
||||
|
||||
// Register the action with menu item after the view item provider
|
||||
// so the toolbar picks up the custom widget
|
||||
this._register(registerAction2(class extends Action2 {
|
||||
@@ -275,6 +297,32 @@ class AccountWidgetContribution extends Disposable implements IWorkbenchContribu
|
||||
// Handled by the custom view item
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: sessionsUpdateWidgetAction,
|
||||
title: localize2('sessionsUpdateWidget', 'Sessions Update'),
|
||||
menu: {
|
||||
id: Menus.SidebarFooter,
|
||||
group: 'navigation',
|
||||
order: 0,
|
||||
when: ContextKeyExpr.or(
|
||||
CONTEXT_UPDATE_STATE.isEqualTo(StateType.Ready),
|
||||
CONTEXT_UPDATE_STATE.isEqualTo(StateType.AvailableForDownload),
|
||||
CONTEXT_UPDATE_STATE.isEqualTo(StateType.CheckingForUpdates),
|
||||
CONTEXT_UPDATE_STATE.isEqualTo(StateType.Downloading),
|
||||
CONTEXT_UPDATE_STATE.isEqualTo(StateType.Downloaded),
|
||||
CONTEXT_UPDATE_STATE.isEqualTo(StateType.Updating),
|
||||
CONTEXT_UPDATE_STATE.isEqualTo(StateType.Overwriting),
|
||||
)
|
||||
}
|
||||
});
|
||||
}
|
||||
async run(): Promise<void> {
|
||||
// Handled by the custom view item
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,19 +3,6 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Account Widget */
|
||||
.monaco-workbench .part.sidebar > .sidebar-footer .account-widget > .action-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.sidebar > .sidebar-footer .account-widget {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Account Button */
|
||||
.monaco-workbench .part.sidebar > .sidebar-footer .account-widget-account {
|
||||
overflow: hidden;
|
||||
@@ -23,49 +10,9 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.sidebar > .sidebar-footer .account-widget-account-button {
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
height: auto;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background: transparent;
|
||||
color: var(--vscode-sideBar-foreground);
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
justify-content: flex-start;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.sidebar > .sidebar-footer .account-widget-account-button:hover {
|
||||
background-color: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
/* Update Button */
|
||||
.monaco-workbench .part.sidebar > .sidebar-footer .account-widget-update {
|
||||
.monaco-workbench .part.sidebar > .sidebar-footer .update-widget-action {
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.sidebar > .sidebar-footer .account-widget-update-button {
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
height: auto;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background: transparent;
|
||||
color: var(--vscode-sideBar-foreground);
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
justify-content: flex-start;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.monaco-workbench .part.sidebar > .sidebar-footer .account-widget-update-button:hover:not(:disabled) {
|
||||
background-color: var(--vscode-toolbar-hoverBackground);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
+4
-4
@@ -38,11 +38,11 @@
|
||||
.ai-customization-management-editor .section-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
padding: 4px 8px;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
margin: 2px 6px;
|
||||
border-radius: 6px;
|
||||
margin: 1px 6px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.1s ease, opacity 0.1s ease;
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js';
|
||||
import { IPromptsService, PromptsStorage } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';
|
||||
import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js';
|
||||
|
||||
export interface ISourceCounts {
|
||||
readonly workspace: number;
|
||||
readonly user: number;
|
||||
readonly extension: number;
|
||||
}
|
||||
|
||||
export function getSourceCountsTotal(counts: ISourceCounts): number {
|
||||
return counts.workspace + counts.user + counts.extension;
|
||||
}
|
||||
|
||||
export async function getPromptSourceCounts(promptsService: IPromptsService, promptType: PromptsType): Promise<ISourceCounts> {
|
||||
const [workspaceItems, userItems, extensionItems] = await Promise.all([
|
||||
promptsService.listPromptFilesForStorage(promptType, PromptsStorage.local, CancellationToken.None),
|
||||
promptsService.listPromptFilesForStorage(promptType, PromptsStorage.user, CancellationToken.None),
|
||||
promptsService.listPromptFilesForStorage(promptType, PromptsStorage.extension, CancellationToken.None),
|
||||
]);
|
||||
return {
|
||||
workspace: workspaceItems.length,
|
||||
user: userItems.length,
|
||||
extension: extensionItems.length,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getSkillSourceCounts(promptsService: IPromptsService): Promise<ISourceCounts> {
|
||||
const skills = await promptsService.findAgentSkills(CancellationToken.None);
|
||||
if (!skills || skills.length === 0) {
|
||||
return { workspace: 0, user: 0, extension: 0 };
|
||||
}
|
||||
return {
|
||||
workspace: skills.filter(s => s.storage === PromptsStorage.local).length,
|
||||
user: skills.filter(s => s.storage === PromptsStorage.user).length,
|
||||
extension: skills.filter(s => s.storage === PromptsStorage.extension).length,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getCustomizationTotalCount(promptsService: IPromptsService, mcpService: IMcpService): Promise<number> {
|
||||
const [agentCounts, skillCounts, instructionCounts, promptCounts, hookCounts] = await Promise.all([
|
||||
getPromptSourceCounts(promptsService, PromptsType.agent),
|
||||
getSkillSourceCounts(promptsService),
|
||||
getPromptSourceCounts(promptsService, PromptsType.instructions),
|
||||
getPromptSourceCounts(promptsService, PromptsType.prompt),
|
||||
getPromptSourceCounts(promptsService, PromptsType.hook),
|
||||
]);
|
||||
|
||||
return getSourceCountsTotal(agentCounts)
|
||||
+ getSourceCountsTotal(skillCounts)
|
||||
+ getSourceCountsTotal(instructionCounts)
|
||||
+ getSourceCountsTotal(promptCounts)
|
||||
+ getSourceCountsTotal(hookCounts)
|
||||
+ mcpService.servers.get().length;
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, localize2 } from '../../../../nls.js';
|
||||
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js';
|
||||
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js';
|
||||
import { IEditorGroupsService } from '../../../../workbench/services/editor/common/editorGroupsService.js';
|
||||
import { AICustomizationManagementEditor } from '../../aiCustomizationManagement/browser/aiCustomizationManagementEditor.js';
|
||||
import { AICustomizationManagementSection } from '../../aiCustomizationManagement/browser/aiCustomizationManagement.js';
|
||||
import { AICustomizationManagementEditorInput } from '../../aiCustomizationManagement/browser/aiCustomizationManagementEditorInput.js';
|
||||
import { IPromptsService } 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 { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js';
|
||||
import { Menus } from '../../../browser/menus.js';
|
||||
import { agentIcon, instructionsIcon, promptIcon, skillIcon, hookIcon, workspaceIcon, userIcon, extensionIcon } from '../../aiCustomizationTreeView/browser/aiCustomizationTreeViewIcons.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 { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { ISessionsManagementService } from './sessionsManagementService.js';
|
||||
import { Button } from '../../../../base/browser/ui/button/button.js';
|
||||
import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js';
|
||||
import { getPromptSourceCounts, getSkillSourceCounts, getSourceCountsTotal, ISourceCounts } from './customizationCounts.js';
|
||||
|
||||
interface ICustomizationItemConfig {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly icon: ThemeIcon;
|
||||
readonly section: AICustomizationManagementSection;
|
||||
readonly getSourceCounts?: (promptsService: IPromptsService) => Promise<ISourceCounts>;
|
||||
readonly getCount?: (languageModelsService: ILanguageModelsService, mcpService: IMcpService) => Promise<number>;
|
||||
}
|
||||
|
||||
const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [
|
||||
{
|
||||
id: 'sessions.customization.agents',
|
||||
label: localize('agents', "Agents"),
|
||||
icon: agentIcon,
|
||||
section: AICustomizationManagementSection.Agents,
|
||||
getSourceCounts: (ps) => getPromptSourceCounts(ps, PromptsType.agent),
|
||||
},
|
||||
{
|
||||
id: 'sessions.customization.skills',
|
||||
label: localize('skills', "Skills"),
|
||||
icon: skillIcon,
|
||||
section: AICustomizationManagementSection.Skills,
|
||||
getSourceCounts: (ps) => getSkillSourceCounts(ps),
|
||||
},
|
||||
{
|
||||
id: 'sessions.customization.instructions',
|
||||
label: localize('instructions', "Instructions"),
|
||||
icon: instructionsIcon,
|
||||
section: AICustomizationManagementSection.Instructions,
|
||||
getSourceCounts: (ps) => getPromptSourceCounts(ps, PromptsType.instructions),
|
||||
},
|
||||
{
|
||||
id: 'sessions.customization.prompts',
|
||||
label: localize('prompts', "Prompts"),
|
||||
icon: promptIcon,
|
||||
section: AICustomizationManagementSection.Prompts,
|
||||
getSourceCounts: (ps) => getPromptSourceCounts(ps, PromptsType.prompt),
|
||||
},
|
||||
{
|
||||
id: 'sessions.customization.hooks',
|
||||
label: localize('hooks', "Hooks"),
|
||||
icon: hookIcon,
|
||||
section: AICustomizationManagementSection.Hooks,
|
||||
getSourceCounts: (ps) => getPromptSourceCounts(ps, PromptsType.hook),
|
||||
},
|
||||
{
|
||||
id: 'sessions.customization.mcpServers',
|
||||
label: localize('mcpServers', "MCP Servers"),
|
||||
icon: Codicon.server,
|
||||
section: AICustomizationManagementSection.McpServers,
|
||||
getCount: (_lm, mcp) => Promise.resolve(mcp.servers.get().length),
|
||||
},
|
||||
{
|
||||
id: 'sessions.customization.models',
|
||||
label: localize('models', "Models"),
|
||||
icon: Codicon.vm,
|
||||
section: AICustomizationManagementSection.Models,
|
||||
getCount: (lm) => Promise.resolve(lm.getLanguageModelIds().length),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Custom ActionViewItem for each customization link in the toolbar.
|
||||
* Renders icon + label + source count badges, matching the sidebar footer style.
|
||||
*/
|
||||
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,
|
||||
@IPromptsService private readonly _promptsService: IPromptsService,
|
||||
@ILanguageModelsService private readonly _languageModelsService: ILanguageModelsService,
|
||||
@IMcpService private readonly _mcpService: IMcpService,
|
||||
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
|
||||
@ISessionsManagementService private readonly _activeSessionService: ISessionsManagementService,
|
||||
) {
|
||||
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'));
|
||||
|
||||
// Subscribe to changes
|
||||
this._viewItemDisposables.add(this._promptsService.onDidChangeCustomAgents(() => this._updateCounts()));
|
||||
this._viewItemDisposables.add(this._promptsService.onDidChangeSlashCommands(() => this._updateCounts()));
|
||||
this._viewItemDisposables.add(this._languageModelsService.onDidChangeLanguageModels(() => this._updateCounts()));
|
||||
this._viewItemDisposables.add(autorun(reader => {
|
||||
this._mcpService.servers.read(reader);
|
||||
this._updateCounts();
|
||||
}));
|
||||
this._viewItemDisposables.add(this._workspaceContextService.onDidChangeWorkspaceFolders(() => this._updateCounts()));
|
||||
this._viewItemDisposables.add(autorun(reader => {
|
||||
this._activeSessionService.activeSession.read(reader);
|
||||
this._updateCounts();
|
||||
}));
|
||||
|
||||
// Initial count
|
||||
this._updateCounts();
|
||||
}
|
||||
|
||||
private async _updateCounts(): Promise<void> {
|
||||
if (!this._countContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._config.getSourceCounts) {
|
||||
const counts = await this._config.getSourceCounts(this._promptsService);
|
||||
this._renderSourceCounts(this._countContainer, counts);
|
||||
} else if (this._config.getCount) {
|
||||
const count = await this._config.getCount(this._languageModelsService, this._mcpService);
|
||||
this._renderSimpleCount(this._countContainer, count);
|
||||
}
|
||||
}
|
||||
|
||||
private _renderSourceCounts(container: HTMLElement, counts: ISourceCounts): void {
|
||||
container.textContent = '';
|
||||
const total = getSourceCountsTotal(counts);
|
||||
container.classList.toggle('hidden', total === 0);
|
||||
if (total === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sources: { count: number; icon: ThemeIcon; title: string }[] = [
|
||||
{ count: counts.workspace, icon: workspaceIcon, title: localize('workspaceCount', "{0} from workspace", counts.workspace) },
|
||||
{ count: counts.user, icon: userIcon, title: localize('userCount', "{0} from user", counts.user) },
|
||||
{ count: counts.extension, icon: extensionIcon, title: localize('extensionCount', "{0} from extensions", counts.extension) },
|
||||
];
|
||||
|
||||
for (const source of sources) {
|
||||
if (source.count === 0) {
|
||||
continue;
|
||||
}
|
||||
const badge = append(container, $('span.source-count-badge'));
|
||||
badge.title = source.title;
|
||||
const icon = append(badge, $('span.source-count-icon'));
|
||||
icon.classList.add(...ThemeIcon.asClassNameArray(source.icon));
|
||||
const num = append(badge, $('span.source-count-num'));
|
||||
num.textContent = `${source.count}`;
|
||||
}
|
||||
}
|
||||
|
||||
private _renderSimpleCount(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 --- //
|
||||
|
||||
class CustomizationsToolbarContribution extends Disposable implements IWorkbenchContribution {
|
||||
|
||||
static readonly ID = 'workbench.contrib.sessionsCustomizationsToolbar';
|
||||
|
||||
constructor(
|
||||
@IActionViewItemService actionViewItemService: IActionViewItemService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
|
||||
for (const [index, config] of CUSTOMIZATION_ITEMS.entries()) {
|
||||
// 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));
|
||||
|
||||
// Register the action with menu item
|
||||
this._register(registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: config.id,
|
||||
title: localize2('customizationAction', '{0}', config.label),
|
||||
menu: {
|
||||
id: Menus.SidebarCustomizations,
|
||||
group: 'navigation',
|
||||
order: index + 1,
|
||||
}
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const editorGroupsService = accessor.get(IEditorGroupsService);
|
||||
const input = AICustomizationManagementEditorInput.getOrCreate();
|
||||
const editor = await editorGroupsService.activeGroup.openEditor(input, { pinned: true });
|
||||
if (editor instanceof AICustomizationManagementEditor) {
|
||||
editor.selectSectionById(config.section);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerWorkbenchContribution2(CustomizationsToolbarContribution.ID, CustomizationsToolbarContribution, WorkbenchPhase.AfterRestored);
|
||||
@@ -0,0 +1,133 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
}
|
||||
|
||||
/* Section headers - more prominent than time-based groupings */
|
||||
.ai-customization-header,
|
||||
.agent-sessions-header {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
@@ -23,130 +22,17 @@
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* Customization header - clickable for collapse */
|
||||
.ai-customization-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
margin: 0 6px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.ai-customization-header:hover {
|
||||
background-color: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.ai-customization-header:focus {
|
||||
outline: 1px solid var(--vscode-focusBorder);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.ai-customization-chevron,
|
||||
.agent-sessions-chevron {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
padding-right: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.ai-customization-header:hover .ai-customization-chevron,
|
||||
.ai-customization-header:focus .ai-customization-chevron,
|
||||
.agent-sessions-header:hover .agent-sessions-chevron,
|
||||
.agent-sessions-header:focus .agent-sessions-chevron {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* AI Customization section - pinned to bottom */
|
||||
.ai-customization-shortcuts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
border-top: 1px solid var(--vscode-widget-border);
|
||||
margin-top: 8px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.ai-customization-shortcuts .ai-customization-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 500px;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.2s ease-out;
|
||||
}
|
||||
|
||||
.ai-customization-shortcuts .ai-customization-links.collapsed {
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
.ai-customization-shortcuts .ai-customization-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 13px;
|
||||
color: var(--vscode-foreground);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
padding: 6px 14px;
|
||||
margin: 0 6px;
|
||||
line-height: 22px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.ai-customization-shortcuts .ai-customization-link:hover {
|
||||
background-color: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.ai-customization-shortcuts .ai-customization-link:focus {
|
||||
outline: 1px solid var(--vscode-focusBorder);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.ai-customization-shortcuts .ai-customization-link .link-icon {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.ai-customization-shortcuts .ai-customization-link .link-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ai-customization-shortcuts .ai-customization-link .link-counts {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.ai-customization-shortcuts .ai-customization-link .link-counts.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ai-customization-shortcuts .ai-customization-link .source-count-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.ai-customization-shortcuts .ai-customization-link .source-count-icon {
|
||||
font-size: 12px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.ai-customization-shortcuts .ai-customization-link .source-count-num {
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Sessions section - fills remaining space above customizations */
|
||||
.agent-sessions-section {
|
||||
display: flex;
|
||||
@@ -162,6 +48,7 @@
|
||||
gap: 4px;
|
||||
padding-top: 10px;
|
||||
padding-right: 12px;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,53 +3,106 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { autorun } from '../../../../base/common/observable.js';
|
||||
import { Disposable, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { autorun, derivedOpts } from '../../../../base/common/observable.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { ResourceMap } from '../../../../base/common/map.js';
|
||||
import { isEqual } from '../../../../base/common/resources.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js';
|
||||
import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js';
|
||||
import { IChatService } from '../../../../workbench/contrib/chat/common/chatService/chatService.js';
|
||||
import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js';
|
||||
import { getChatSessionType } from '../../../../workbench/contrib/chat/common/model/chatUri.js';
|
||||
import { IWorkbenchLayoutService, Parts } from '../../../../workbench/services/layout/browser/layoutService.js';
|
||||
import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js';
|
||||
import { CHANGES_VIEW_ID, ChangesViewPane } from '../../changesView/browser/changesView.js';
|
||||
import { ISessionsManagementService } from './sessionsManagementService.js';
|
||||
|
||||
interface IPendingTurnState {
|
||||
readonly hadChangesBeforeSend: boolean;
|
||||
readonly submittedAt: number;
|
||||
}
|
||||
|
||||
export class SessionsAuxiliaryBarContribution extends Disposable {
|
||||
|
||||
static readonly ID = 'workbench.contrib.sessionsAuxiliaryBarContribution';
|
||||
|
||||
private readonly activeChangesListener = this._register(new MutableDisposable<IDisposable>());
|
||||
private activeChangesView: ChangesViewPane | null = null;
|
||||
private readonly pendingTurnStateByResource = new ResourceMap<IPendingTurnState>();
|
||||
|
||||
constructor(
|
||||
@IViewsService private readonly viewsService: IViewsService,
|
||||
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
|
||||
@ISessionsManagementService private readonly sessionManagementService: ISessionsManagementService,
|
||||
@IChatEditingService private readonly chatEditingService: IChatEditingService,
|
||||
@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,
|
||||
@IChatService private readonly chatService: IChatService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.tryBindToChangesView();
|
||||
const activeSessionResourceObs = derivedOpts<URI | undefined>({
|
||||
equalsFn: isEqual,
|
||||
}, (reader) => {
|
||||
return this.sessionManagementService.activeSession.map(activeSession => activeSession?.resource).read(reader);
|
||||
}).recomputeInitiallyAndOnChange(this._store);
|
||||
|
||||
this._register(this.viewsService.onDidChangeViewVisibility(e => {
|
||||
if (e.id !== CHANGES_VIEW_ID) {
|
||||
this._register(this.chatService.onDidSubmitRequest(({ chatSessionResource }) => {
|
||||
this.pendingTurnStateByResource.set(chatSessionResource, {
|
||||
hadChangesBeforeSend: this.hasSessionChanges(chatSessionResource),
|
||||
submittedAt: Date.now(),
|
||||
});
|
||||
}));
|
||||
|
||||
// When a turn is completed, check if there were changes before the turn and if there are changes after the turn.
|
||||
// If there were no changes before the turn and there are changes after the turn, show the auxiliary bar.
|
||||
this._register(autorun((reader) => {
|
||||
const activeSessionResource = activeSessionResourceObs.read(reader);
|
||||
if (!activeSessionResource) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tryBindToChangesView();
|
||||
const pendingTurnState = this.pendingTurnStateByResource.get(activeSessionResource);
|
||||
if (!pendingTurnState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeSession = this.agentSessionsService.getSession(activeSessionResource);
|
||||
const turnCompleted = !!activeSession?.timing.lastRequestEnded && activeSession.timing.lastRequestEnded >= pendingTurnState.submittedAt;
|
||||
if (!turnCompleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasChangesAfterTurn = this.hasSessionChanges(activeSessionResource);
|
||||
if (!pendingTurnState.hadChangesBeforeSend && hasChangesAfterTurn) {
|
||||
this.layoutService.setPartHidden(false, Parts.AUXILIARYBAR_PART);
|
||||
}
|
||||
|
||||
this.pendingTurnStateByResource.delete(activeSessionResource);
|
||||
}));
|
||||
|
||||
// When the session is switched, show the auxiliary bar if there are pending changes from the session
|
||||
this._register(autorun(reader => {
|
||||
const sessionResource = activeSessionResourceObs.read(reader);
|
||||
if (!sessionResource) {
|
||||
this.syncAuxiliaryBarVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const hasChanges = this.hasSessionChanges(sessionResource);
|
||||
this.syncAuxiliaryBarVisibility(hasChanges);
|
||||
}));
|
||||
}
|
||||
|
||||
private tryBindToChangesView(): void {
|
||||
const changesView = this.viewsService.getViewWithId<ChangesViewPane>(CHANGES_VIEW_ID);
|
||||
if (!changesView) {
|
||||
this.activeChangesView = null;
|
||||
this.activeChangesListener.clear();
|
||||
return;
|
||||
private hasSessionChanges(sessionResource: URI): boolean {
|
||||
const isBackgroundSession = getChatSessionType(sessionResource) === AgentSessionProviders.Background;
|
||||
|
||||
let editingSessionCount = 0;
|
||||
if (!isBackgroundSession) {
|
||||
const sessions = this.chatEditingService.editingSessionsObs.read(undefined);
|
||||
const editingSession = sessions.find(candidate => isEqual(candidate.chatSessionResource, sessionResource));
|
||||
editingSessionCount = editingSession ? editingSession.entries.read(undefined).length : 0;
|
||||
}
|
||||
|
||||
if (this.activeChangesView === changesView) {
|
||||
return;
|
||||
}
|
||||
const session = this.agentSessionsService.getSession(sessionResource);
|
||||
const sessionFilesCount = session?.changes instanceof Array ? session.changes.length : 0;
|
||||
|
||||
this.activeChangesView = changesView;
|
||||
this.activeChangesListener.value = autorun(reader => {
|
||||
const hasChanges = changesView.activeSessionHasChanges.read(reader);
|
||||
this.syncAuxiliaryBarVisibility(hasChanges);
|
||||
});
|
||||
return editingSessionCount + sessionFilesCount > 0;
|
||||
}
|
||||
|
||||
private syncAuxiliaryBarVisibility(hasChanges: boolean): void {
|
||||
@@ -61,4 +114,4 @@ export class SessionsAuxiliaryBarContribution extends Disposable {
|
||||
|
||||
this.layoutService.setPartHidden(shouldHideAuxiliaryBar, Parts.AUXILIARYBAR_PART);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
* 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 { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
|
||||
import { autorun } from '../../../../base/common/observable.js';
|
||||
@@ -23,51 +24,27 @@ import { IHoverService } from '../../../../platform/hover/browser/hover.js';
|
||||
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 { IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';
|
||||
import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js';
|
||||
import { ISessionsManagementService } from './sessionsManagementService.js';
|
||||
import { Action2, ISubmenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js';
|
||||
import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/browser/layoutService.js';
|
||||
import { Button } from '../../../../base/browser/ui/button/button.js';
|
||||
import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { ACTION_ID_NEW_CHAT } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js';
|
||||
import { IEditorGroupsService } from '../../../../workbench/services/editor/common/editorGroupsService.js';
|
||||
import { AICustomizationManagementEditor } from '../../aiCustomizationManagement/browser/aiCustomizationManagementEditor.js';
|
||||
import { AICustomizationManagementSection } from '../../aiCustomizationManagement/browser/aiCustomizationManagement.js';
|
||||
import { AICustomizationManagementEditorInput } from '../../aiCustomizationManagement/browser/aiCustomizationManagementEditorInput.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 { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.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 { agentIcon, instructionsIcon, promptIcon, skillIcon, hookIcon, workspaceIcon, userIcon, extensionIcon } from '../../aiCustomizationTreeView/browser/aiCustomizationTreeViewIcons.js';
|
||||
import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';
|
||||
import { Menus } from '../../../browser/menus.js';
|
||||
import { getCustomizationTotalCount } from './customizationCounts.js';
|
||||
|
||||
const $ = DOM.$;
|
||||
export const SessionsViewId = 'agentic.workbench.view.sessionsView';
|
||||
const SessionsViewFilterSubMenu = new MenuId('AgentSessionsViewFilterSubMenu');
|
||||
|
||||
/**
|
||||
* Per-source breakdown of item counts.
|
||||
*/
|
||||
interface ISourceCounts {
|
||||
readonly workspace: number;
|
||||
readonly user: number;
|
||||
readonly extension: number;
|
||||
}
|
||||
|
||||
interface IShortcutItem {
|
||||
readonly label: string;
|
||||
readonly icon: ThemeIcon;
|
||||
readonly action: () => Promise<void>;
|
||||
readonly getSourceCounts?: () => Promise<ISourceCounts>;
|
||||
/** For items without per-source breakdown (MCP, Models). */
|
||||
readonly getCount?: () => Promise<number>;
|
||||
countContainer?: HTMLElement;
|
||||
}
|
||||
|
||||
const CUSTOMIZATIONS_COLLAPSED_KEY = 'agentSessions.customizationsCollapsed';
|
||||
|
||||
export class AgenticSessionsViewPane extends ViewPane {
|
||||
@@ -76,7 +53,6 @@ export class AgenticSessionsViewPane extends ViewPane {
|
||||
private sessionsControlContainer: HTMLElement | undefined;
|
||||
sessionsControl: AgentSessionsControl | undefined;
|
||||
private aiCustomizationContainer: HTMLElement | undefined;
|
||||
private readonly shortcuts: IShortcutItem[] = [];
|
||||
|
||||
constructor(
|
||||
options: IViewPaneOptions,
|
||||
@@ -90,44 +66,13 @@ export class AgenticSessionsViewPane extends ViewPane {
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IHoverService hoverService: IHoverService,
|
||||
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
|
||||
@ICommandService commandService: ICommandService,
|
||||
@IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService,
|
||||
@IPromptsService private readonly promptsService: IPromptsService,
|
||||
@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,
|
||||
@IMcpService private readonly mcpService: IMcpService,
|
||||
@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,
|
||||
) {
|
||||
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
|
||||
|
||||
// Initialize shortcuts
|
||||
this.shortcuts = [
|
||||
{ label: localize('agents', "Agents"), icon: agentIcon, action: () => this.openAICustomizationSection(AICustomizationManagementSection.Agents), getSourceCounts: () => this.getPromptSourceCounts(PromptsType.agent) },
|
||||
{ label: localize('skills', "Skills"), icon: skillIcon, action: () => this.openAICustomizationSection(AICustomizationManagementSection.Skills), getSourceCounts: () => this.getSkillSourceCounts() },
|
||||
{ label: localize('instructions', "Instructions"), icon: instructionsIcon, action: () => this.openAICustomizationSection(AICustomizationManagementSection.Instructions), getSourceCounts: () => this.getPromptSourceCounts(PromptsType.instructions) },
|
||||
{ label: localize('prompts', "Prompts"), icon: promptIcon, action: () => this.openAICustomizationSection(AICustomizationManagementSection.Prompts), getSourceCounts: () => this.getPromptSourceCounts(PromptsType.prompt) },
|
||||
{ label: localize('hooks', "Hooks"), icon: hookIcon, action: () => this.openAICustomizationSection(AICustomizationManagementSection.Hooks), getSourceCounts: () => this.getPromptSourceCounts(PromptsType.hook) },
|
||||
{ label: localize('mcpServers', "MCP Servers"), icon: Codicon.server, action: () => this.openAICustomizationSection(AICustomizationManagementSection.McpServers), getCount: () => Promise.resolve(this.mcpService.servers.get().length) },
|
||||
{ label: localize('models', "Models"), icon: Codicon.vm, action: () => this.openAICustomizationSection(AICustomizationManagementSection.Models), getCount: () => Promise.resolve(this.languageModelsService.getLanguageModelIds().length) },
|
||||
];
|
||||
|
||||
// Listen to changes to update counts
|
||||
this._register(this.promptsService.onDidChangeCustomAgents(() => this.updateCounts()));
|
||||
this._register(this.promptsService.onDidChangeSlashCommands(() => this.updateCounts()));
|
||||
this._register(this.languageModelsService.onDidChangeLanguageModels(() => this.updateCounts()));
|
||||
this._register(autorun(reader => {
|
||||
this.mcpService.servers.read(reader);
|
||||
this.updateCounts();
|
||||
}));
|
||||
|
||||
// Listen to workspace folder changes to update counts
|
||||
this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(() => this.updateCounts()));
|
||||
this._register(autorun(reader => {
|
||||
this.activeSessionService.activeSession.read(reader);
|
||||
this.updateCounts();
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
protected override renderBody(parent: HTMLElement): void {
|
||||
@@ -199,8 +144,8 @@ export class AgenticSessionsViewPane extends ViewPane {
|
||||
}
|
||||
}));
|
||||
|
||||
// AI Customization shortcuts (bottom, fixed height)
|
||||
this.aiCustomizationContainer = DOM.append(sessionsContainer, $('.ai-customization-shortcuts'));
|
||||
// AI Customization toolbar (bottom, fixed height)
|
||||
this.aiCustomizationContainer = DOM.append(sessionsContainer, $('div'));
|
||||
this.createAICustomizationShortcuts(this.aiCustomizationContainer);
|
||||
}
|
||||
|
||||
@@ -215,177 +160,86 @@ export class AgenticSessionsViewPane extends ViewPane {
|
||||
// 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.tabIndex = 0;
|
||||
header.setAttribute('role', 'button');
|
||||
header.setAttribute('aria-expanded', String(!isCollapsed));
|
||||
header.classList.toggle('collapsed', isCollapsed);
|
||||
|
||||
// Header text
|
||||
const headerText = DOM.append(header, $('span'));
|
||||
headerText.textContent = localize('customizations', "CUSTOMIZATIONS");
|
||||
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");
|
||||
|
||||
// Chevron icon (right-aligned, shown on hover)
|
||||
const chevron = DOM.append(header, $('.ai-customization-chevron'));
|
||||
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));
|
||||
|
||||
// Links container
|
||||
const linksContainer = DOM.append(container, $('.ai-customization-links'));
|
||||
if (isCollapsed) {
|
||||
linksContainer.classList.add('collapsed');
|
||||
}
|
||||
// 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);
|
||||
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();
|
||||
}));
|
||||
updateHeaderTotalCount();
|
||||
|
||||
// Toggle collapse on header click
|
||||
const toggleCollapse = () => {
|
||||
const collapsed = linksContainer.classList.toggle('collapsed');
|
||||
const collapsed = container.classList.toggle('collapsed');
|
||||
header.classList.toggle('collapsed', collapsed);
|
||||
this.storageService.store(CUSTOMIZATIONS_COLLAPSED_KEY, collapsed, StorageScope.PROFILE, StorageTarget.USER);
|
||||
header.setAttribute('aria-expanded', String(!collapsed));
|
||||
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
|
||||
const onTransitionEnd = () => {
|
||||
linksContainer.removeEventListener('transitionend', onTransitionEnd);
|
||||
toolbarContainer.removeEventListener('transitionend', onTransitionEnd);
|
||||
if (this.viewPaneContainer) {
|
||||
const { offsetHeight, offsetWidth } = this.viewPaneContainer;
|
||||
this.layoutBody(offsetHeight, offsetWidth);
|
||||
}
|
||||
};
|
||||
linksContainer.addEventListener('transitionend', onTransitionEnd);
|
||||
toolbarContainer.addEventListener('transitionend', onTransitionEnd);
|
||||
};
|
||||
|
||||
this._register(DOM.addDisposableListener(header, 'click', toggleCollapse));
|
||||
this._register(DOM.addDisposableListener(header, 'keydown', (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggleCollapse();
|
||||
}
|
||||
}));
|
||||
|
||||
for (const shortcut of this.shortcuts) {
|
||||
const link = DOM.append(linksContainer, $('a.ai-customization-link'));
|
||||
link.tabIndex = 0;
|
||||
link.setAttribute('role', 'button');
|
||||
link.setAttribute('aria-label', shortcut.label);
|
||||
|
||||
// Icon
|
||||
const iconElement = DOM.append(link, $('.link-icon'));
|
||||
iconElement.classList.add(...ThemeIcon.asClassNameArray(shortcut.icon));
|
||||
|
||||
// Label
|
||||
const labelElement = DOM.append(link, $('.link-label'));
|
||||
labelElement.textContent = shortcut.label;
|
||||
|
||||
// Count container (right-aligned, shows per-source badges)
|
||||
const countContainer = DOM.append(link, $('.link-counts'));
|
||||
shortcut.countContainer = countContainer;
|
||||
|
||||
this._register(DOM.addDisposableListener(link, 'click', (e) => {
|
||||
DOM.EventHelper.stop(e);
|
||||
shortcut.action();
|
||||
}));
|
||||
|
||||
this._register(DOM.addDisposableListener(link, 'keydown', (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
shortcut.action();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Load initial counts
|
||||
this.updateCounts();
|
||||
}
|
||||
|
||||
private async updateCounts(): Promise<void> {
|
||||
for (const shortcut of this.shortcuts) {
|
||||
if (!shortcut.countContainer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (shortcut.getSourceCounts) {
|
||||
const counts = await shortcut.getSourceCounts();
|
||||
this.renderSourceCounts(shortcut.countContainer, counts);
|
||||
} else if (shortcut.getCount) {
|
||||
const count = await shortcut.getCount();
|
||||
this.renderSimpleCount(shortcut.countContainer, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private renderSourceCounts(container: HTMLElement, counts: ISourceCounts): void {
|
||||
DOM.clearNode(container);
|
||||
const total = counts.workspace + counts.user + counts.extension;
|
||||
container.classList.toggle('hidden', total === 0);
|
||||
if (total === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sources: { count: number; icon: ThemeIcon; title: string }[] = [
|
||||
{ count: counts.workspace, icon: workspaceIcon, title: localize('workspaceCount', "{0} from workspace", counts.workspace) },
|
||||
{ count: counts.user, icon: userIcon, title: localize('userCount', "{0} from user", counts.user) },
|
||||
{ count: counts.extension, icon: extensionIcon, title: localize('extensionCount', "{0} from extensions", counts.extension) },
|
||||
];
|
||||
|
||||
for (const source of sources) {
|
||||
if (source.count === 0) {
|
||||
continue;
|
||||
}
|
||||
const badge = DOM.append(container, $('.source-count-badge'));
|
||||
badge.title = source.title;
|
||||
const icon = DOM.append(badge, $('.source-count-icon'));
|
||||
icon.classList.add(...ThemeIcon.asClassNameArray(source.icon));
|
||||
const num = DOM.append(badge, $('.source-count-num'));
|
||||
num.textContent = `${source.count}`;
|
||||
}
|
||||
}
|
||||
|
||||
private renderSimpleCount(container: HTMLElement, count: number): void {
|
||||
DOM.clearNode(container);
|
||||
container.classList.toggle('hidden', count === 0);
|
||||
if (count > 0) {
|
||||
const badge = DOM.append(container, $('.source-count-badge'));
|
||||
const num = DOM.append(badge, $('.source-count-num'));
|
||||
num.textContent = `${count}`;
|
||||
}
|
||||
}
|
||||
|
||||
private async getPromptSourceCounts(promptType: PromptsType): Promise<ISourceCounts> {
|
||||
const [workspaceItems, userItems, extensionItems] = await Promise.all([
|
||||
this.promptsService.listPromptFilesForStorage(promptType, PromptsStorage.local, CancellationToken.None),
|
||||
this.promptsService.listPromptFilesForStorage(promptType, PromptsStorage.user, CancellationToken.None),
|
||||
this.promptsService.listPromptFilesForStorage(promptType, PromptsStorage.extension, CancellationToken.None),
|
||||
]);
|
||||
|
||||
return {
|
||||
workspace: workspaceItems.length,
|
||||
user: userItems.length,
|
||||
extension: extensionItems.length,
|
||||
};
|
||||
}
|
||||
|
||||
private async getSkillSourceCounts(): Promise<ISourceCounts> {
|
||||
const skills = await this.promptsService.findAgentSkills(CancellationToken.None);
|
||||
if (!skills || skills.length === 0) {
|
||||
return { workspace: 0, user: 0, extension: 0 };
|
||||
}
|
||||
|
||||
const workspaceSkills = skills.filter(s => s.storage === PromptsStorage.local);
|
||||
|
||||
return {
|
||||
workspace: workspaceSkills.length,
|
||||
user: skills.filter(s => s.storage === PromptsStorage.user).length,
|
||||
extension: skills.filter(s => s.storage === PromptsStorage.extension).length,
|
||||
};
|
||||
}
|
||||
|
||||
private async openAICustomizationSection(sectionId: AICustomizationManagementSection): Promise<void> {
|
||||
const input = AICustomizationManagementEditorInput.getOrCreate();
|
||||
const editor = await this.editorGroupsService.activeGroup.openEditor(input, { pinned: true });
|
||||
|
||||
if (editor instanceof AICustomizationManagementEditor) {
|
||||
editor.selectSectionById(sectionId);
|
||||
}
|
||||
this._register(headerButton.onDidClick(() => toggleCollapse()));
|
||||
}
|
||||
|
||||
private getSessionHoverPosition(): HoverPosition {
|
||||
|
||||
@@ -191,6 +191,7 @@ import './contrib/aiCustomizationTreeView/browser/aiCustomizationTreeView.contri
|
||||
import './contrib/aiCustomizationManagement/browser/aiCustomizationManagement.contribution.js';
|
||||
import './contrib/chat/browser/chat.contribution.js';
|
||||
import './contrib/sessions/browser/sessions.contribution.js';
|
||||
import './contrib/sessions/browser/customizationsToolbar.contribution.js';
|
||||
import './contrib/changesView/browser/changesView.contribution.js';
|
||||
import './contrib/configuration/browser/configuration.contribution.js';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user