mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-15 07:28:05 +00:00
add tips toolbar (#295175)
This commit is contained in:
@@ -274,6 +274,7 @@ export class MenuId {
|
||||
static readonly ChatTitleBarMenu = new MenuId('ChatTitleBarMenu');
|
||||
static readonly ChatAttachmentsContext = new MenuId('ChatAttachmentsContext');
|
||||
static readonly ChatTipContext = new MenuId('ChatTipContext');
|
||||
static readonly ChatTipToolbar = new MenuId('ChatTipToolbar');
|
||||
static readonly ChatToolOutputResourceToolbar = new MenuId('ChatToolOutputResourceToolbar');
|
||||
static readonly ChatTextEditorMenu = new MenuId('ChatTextEditorMenu');
|
||||
static readonly ChatToolOutputResourceContext = new MenuId('ChatToolOutputResourceContext');
|
||||
|
||||
@@ -37,6 +37,16 @@ export interface IChatTipService {
|
||||
*/
|
||||
readonly onDidDismissTip: Event<void>;
|
||||
|
||||
/**
|
||||
* Fired when the user navigates to a different tip (previous/next).
|
||||
*/
|
||||
readonly onDidNavigateTip: Event<IChatTip>;
|
||||
|
||||
/**
|
||||
* Fired when the tip widget is hidden without dismissing the tip.
|
||||
*/
|
||||
readonly onDidHideTip: Event<void>;
|
||||
|
||||
/**
|
||||
* Fired when tips are disabled.
|
||||
*/
|
||||
@@ -72,10 +82,28 @@ export interface IChatTipService {
|
||||
*/
|
||||
dismissTip(): void;
|
||||
|
||||
/**
|
||||
* Hides the tip widget without permanently dismissing the tip.
|
||||
* The tip may be shown again in a future session.
|
||||
*/
|
||||
hideTip(): void;
|
||||
|
||||
/**
|
||||
* Disables tips permanently by setting the `chat.tips.enabled` configuration to false.
|
||||
*/
|
||||
disableTips(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Navigates to the next tip in the catalog without permanently dismissing the current one.
|
||||
* @param contextKeyService The context key service to evaluate tip eligibility.
|
||||
*/
|
||||
navigateToNextTip(contextKeyService: IContextKeyService): IChatTip | undefined;
|
||||
|
||||
/**
|
||||
* Navigates to the previous tip in the catalog without permanently dismissing the current one.
|
||||
* @param contextKeyService The context key service to evaluate tip eligibility.
|
||||
*/
|
||||
navigateToPreviousTip(contextKeyService: IContextKeyService): IChatTip | undefined;
|
||||
}
|
||||
|
||||
export interface ITipDefinition {
|
||||
@@ -472,6 +500,12 @@ export class ChatTipService extends Disposable implements IChatTipService {
|
||||
private readonly _onDidDismissTip = this._register(new Emitter<void>());
|
||||
readonly onDidDismissTip = this._onDidDismissTip.event;
|
||||
|
||||
private readonly _onDidNavigateTip = this._register(new Emitter<IChatTip>());
|
||||
readonly onDidNavigateTip = this._onDidNavigateTip.event;
|
||||
|
||||
private readonly _onDidHideTip = this._register(new Emitter<void>());
|
||||
readonly onDidHideTip = this._onDidHideTip.event;
|
||||
|
||||
private readonly _onDidDisableTips = this._register(new Emitter<void>());
|
||||
readonly onDidDisableTips = this._onDidDisableTips.event;
|
||||
|
||||
@@ -557,6 +591,13 @@ export class ChatTipService extends Disposable implements IChatTipService {
|
||||
}
|
||||
}
|
||||
|
||||
hideTip(): void {
|
||||
this._hasShownRequestTip = false;
|
||||
this._shownTip = undefined;
|
||||
this._tipRequestId = undefined;
|
||||
this._onDidHideTip.fire();
|
||||
}
|
||||
|
||||
async disableTips(): Promise<void> {
|
||||
this._hasShownRequestTip = false;
|
||||
this._shownTip = undefined;
|
||||
@@ -688,6 +729,40 @@ export class ChatTipService extends Disposable implements IChatTipService {
|
||||
return this._createTip(selectedTip);
|
||||
}
|
||||
|
||||
navigateToNextTip(contextKeyService: IContextKeyService): IChatTip | undefined {
|
||||
return this._navigateTip(1, contextKeyService);
|
||||
}
|
||||
|
||||
navigateToPreviousTip(contextKeyService: IContextKeyService): IChatTip | undefined {
|
||||
return this._navigateTip(-1, contextKeyService);
|
||||
}
|
||||
|
||||
private _navigateTip(direction: 1 | -1, contextKeyService: IContextKeyService): IChatTip | undefined {
|
||||
if (!this._shownTip) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const currentIndex = TIP_CATALOG.findIndex(t => t.id === this._shownTip!.id);
|
||||
if (currentIndex === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const dismissedIds = new Set(this._getDismissedTipIds());
|
||||
for (let i = 1; i < TIP_CATALOG.length; i++) {
|
||||
const idx = ((currentIndex + direction * i) % TIP_CATALOG.length + TIP_CATALOG.length) % TIP_CATALOG.length;
|
||||
const candidate = TIP_CATALOG[idx];
|
||||
if (!dismissedIds.has(candidate.id) && this._isEligible(candidate, contextKeyService)) {
|
||||
this._shownTip = candidate;
|
||||
this._storageService.store(ChatTipService._LAST_TIP_ID_KEY, candidate.id, StorageScope.PROFILE, StorageTarget.USER);
|
||||
const tip = this._createTip(candidate);
|
||||
this._onDidNavigateTip.fire(tip);
|
||||
return tip;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _isEligible(tip: ITipDefinition, contextKeyService: IContextKeyService): boolean {
|
||||
if (tip.when && !contextKeyService.contextMatchesRules(tip.when)) {
|
||||
this._logService.debug('#ChatTips: tip is not eligible due to when clause', tip.id, tip.when.serialize());
|
||||
|
||||
@@ -13,10 +13,11 @@ import { Emitter } from '../../../../../../base/common/event.js';
|
||||
import { Disposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js';
|
||||
import { localize, localize2 } from '../../../../../../nls.js';
|
||||
import { getFlatContextMenuActions } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js';
|
||||
import { MenuWorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js';
|
||||
import { Action2, IMenuService, MenuId, registerAction2 } from '../../../../../../platform/actions/common/actions.js';
|
||||
import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js';
|
||||
import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js';
|
||||
import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IInstantiationService, ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IMarkdownRenderer } from '../../../../../../platform/markdown/browser/markdownRenderer.js';
|
||||
import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js';
|
||||
import { IChatTip, IChatTipService } from '../../chatTipService.js';
|
||||
@@ -30,6 +31,7 @@ export class ChatTipContentPart extends Disposable {
|
||||
public readonly onDidHide = this._onDidHide.event;
|
||||
|
||||
private readonly _renderedContent = this._register(new MutableDisposable());
|
||||
private readonly _toolbar = this._register(new MutableDisposable<MenuWorkbenchToolBar>());
|
||||
|
||||
private readonly _inChatTipContextKey: IContextKey<boolean>;
|
||||
|
||||
@@ -41,6 +43,7 @@ export class ChatTipContentPart extends Disposable {
|
||||
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
|
||||
@IMenuService private readonly _menuService: IMenuService,
|
||||
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -66,6 +69,14 @@ export class ChatTipContentPart extends Disposable {
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this._chatTipService.onDidNavigateTip(tip => {
|
||||
this._renderTip(tip);
|
||||
}));
|
||||
|
||||
this._register(this._chatTipService.onDidHideTip(() => {
|
||||
this._onDidHide.fire();
|
||||
}));
|
||||
|
||||
this._register(this._chatTipService.onDidDisableTips(() => {
|
||||
this._onDidHide.fire();
|
||||
}));
|
||||
@@ -93,10 +104,22 @@ export class ChatTipContentPart extends Disposable {
|
||||
|
||||
private _renderTip(tip: IChatTip): void {
|
||||
dom.clearNode(this.domNode);
|
||||
this._toolbar.clear();
|
||||
|
||||
this.domNode.appendChild(renderIcon(Codicon.lightbulb));
|
||||
const markdownContent = this._renderer.render(tip.content);
|
||||
this._renderedContent.value = markdownContent;
|
||||
this.domNode.appendChild(markdownContent.element);
|
||||
|
||||
// Toolbar with previous, next, and dismiss actions via MenuWorkbenchToolBar
|
||||
const toolbarContainer = $('.chat-tip-toolbar');
|
||||
this._toolbar.value = this._instantiationService.createInstance(MenuWorkbenchToolBar, toolbarContainer, MenuId.ChatTipToolbar, {
|
||||
menuOptions: {
|
||||
shouldForwardArgs: true,
|
||||
},
|
||||
});
|
||||
this.domNode.appendChild(toolbarContainer);
|
||||
|
||||
const textContent = markdownContent.element.textContent ?? localize('chatTip', "Chat tip");
|
||||
const hasLink = /\[.*?\]\(.*?\)/.test(tip.content.value);
|
||||
const ariaLabel = hasLink
|
||||
@@ -107,6 +130,94 @@ export class ChatTipContentPart extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
//#region Tip toolbar actions
|
||||
|
||||
registerAction2(class PreviousTipAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.chat.previousTip',
|
||||
title: localize2('chatTip.previous', "Previous Tip"),
|
||||
icon: Codicon.chevronLeft,
|
||||
f1: false,
|
||||
menu: [{
|
||||
id: MenuId.ChatTipToolbar,
|
||||
group: 'navigation',
|
||||
order: 1,
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
override async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const chatTipService = accessor.get(IChatTipService);
|
||||
const contextKeyService = accessor.get(IContextKeyService);
|
||||
chatTipService.navigateToPreviousTip(contextKeyService);
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class NextTipAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.chat.nextTip',
|
||||
title: localize2('chatTip.next', "Next Tip"),
|
||||
icon: Codicon.chevronRight,
|
||||
f1: false,
|
||||
menu: [{
|
||||
id: MenuId.ChatTipToolbar,
|
||||
group: 'navigation',
|
||||
order: 2,
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
override async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const chatTipService = accessor.get(IChatTipService);
|
||||
const contextKeyService = accessor.get(IContextKeyService);
|
||||
chatTipService.navigateToNextTip(contextKeyService);
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class DismissTipToolbarAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.chat.dismissTipToolbar',
|
||||
title: localize2('chatTip.dismissButton', "Dismiss Tip"),
|
||||
icon: Codicon.check,
|
||||
f1: false,
|
||||
menu: [{
|
||||
id: MenuId.ChatTipToolbar,
|
||||
group: 'navigation',
|
||||
order: 3,
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
override async run(accessor: ServicesAccessor): Promise<void> {
|
||||
accessor.get(IChatTipService).dismissTip();
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class CloseTipToolbarAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'workbench.action.chat.closeTip',
|
||||
title: localize2('chatTip.close', "Close Tips"),
|
||||
icon: Codicon.close,
|
||||
f1: false,
|
||||
menu: [{
|
||||
id: MenuId.ChatTipToolbar,
|
||||
group: 'navigation',
|
||||
order: 4,
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
override async run(accessor: ServicesAccessor): Promise<void> {
|
||||
accessor.get(IChatTipService).hideTip();
|
||||
}
|
||||
});
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Tip context menu actions
|
||||
|
||||
registerAction2(class DismissTipAction extends Action2 {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
font-family: var(--vscode-chat-font-family, inherit);
|
||||
color: var(--vscode-descriptionForeground);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.interactive-item-container .chat-tip-widget .codicon-lightbulb {
|
||||
@@ -37,6 +36,52 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.interactive-item-container .chat-tip-widget .chat-tip-toolbar,
|
||||
.chat-getting-started-tip-container .chat-tip-widget .chat-tip-toolbar {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.interactive-item-container .chat-tip-widget .chat-tip-toolbar > .monaco-toolbar,
|
||||
.chat-getting-started-tip-container .chat-tip-widget .chat-tip-toolbar > .monaco-toolbar {
|
||||
position: absolute;
|
||||
top: -15px;
|
||||
right: 10px;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
background-color: var(--vscode-editorWidget-background);
|
||||
border: 1px solid var(--vscode-chat-requestBorder);
|
||||
border-radius: var(--vscode-cornerRadius-medium);
|
||||
z-index: 100;
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.interactive-item-container .chat-tip-widget:hover .chat-tip-toolbar,
|
||||
.interactive-item-container .chat-tip-widget:focus-within .chat-tip-toolbar,
|
||||
.chat-getting-started-tip-container .chat-tip-widget:hover .chat-tip-toolbar,
|
||||
.chat-getting-started-tip-container .chat-tip-widget:focus-within .chat-tip-toolbar {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.interactive-item-container .chat-tip-widget .chat-tip-toolbar .action-item,
|
||||
.chat-getting-started-tip-container .chat-tip-widget .chat-tip-toolbar .action-item {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin: 1px 2px;
|
||||
}
|
||||
|
||||
.interactive-item-container .chat-tip-widget .chat-tip-toolbar .action-item .action-label,
|
||||
.chat-getting-started-tip-container .chat-tip-widget .chat-tip-toolbar .action-item .action-label {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.interactive-item-container .chat-tip-widget .chat-tip-toolbar .action-item .action-label:hover,
|
||||
.chat-getting-started-tip-container .chat-tip-widget .chat-tip-toolbar .action-item .action-label:hover {
|
||||
background-color: var(--vscode-toolbar-hoverBackground);
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.chat-getting-started-tip-container {
|
||||
margin-bottom: -4px; /* Counter the flex gap */
|
||||
width: 100%;
|
||||
@@ -57,7 +102,7 @@
|
||||
font-size: var(--vscode-chat-font-size-body-s);
|
||||
font-family: var(--vscode-chat-font-family, inherit);
|
||||
color: var(--vscode-descriptionForeground);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chat-getting-started-tip-container .chat-tip-widget a {
|
||||
|
||||
Reference in New Issue
Block a user