Skip to content

Commit 2a69f02

Browse files
authored
add tips toolbar (microsoft#295175)
1 parent 6e326e9 commit 2a69f02

4 files changed

Lines changed: 235 additions & 3 deletions

File tree

src/vs/platform/actions/common/actions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ export class MenuId {
274274
static readonly ChatTitleBarMenu = new MenuId('ChatTitleBarMenu');
275275
static readonly ChatAttachmentsContext = new MenuId('ChatAttachmentsContext');
276276
static readonly ChatTipContext = new MenuId('ChatTipContext');
277+
static readonly ChatTipToolbar = new MenuId('ChatTipToolbar');
277278
static readonly ChatToolOutputResourceToolbar = new MenuId('ChatToolOutputResourceToolbar');
278279
static readonly ChatTextEditorMenu = new MenuId('ChatTextEditorMenu');
279280
static readonly ChatToolOutputResourceContext = new MenuId('ChatToolOutputResourceContext');

src/vs/workbench/contrib/chat/browser/chatTipService.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ export interface IChatTipService {
3737
*/
3838
readonly onDidDismissTip: Event<void>;
3939

40+
/**
41+
* Fired when the user navigates to a different tip (previous/next).
42+
*/
43+
readonly onDidNavigateTip: Event<IChatTip>;
44+
45+
/**
46+
* Fired when the tip widget is hidden without dismissing the tip.
47+
*/
48+
readonly onDidHideTip: Event<void>;
49+
4050
/**
4151
* Fired when tips are disabled.
4252
*/
@@ -72,10 +82,28 @@ export interface IChatTipService {
7282
*/
7383
dismissTip(): void;
7484

85+
/**
86+
* Hides the tip widget without permanently dismissing the tip.
87+
* The tip may be shown again in a future session.
88+
*/
89+
hideTip(): void;
90+
7591
/**
7692
* Disables tips permanently by setting the `chat.tips.enabled` configuration to false.
7793
*/
7894
disableTips(): Promise<void>;
95+
96+
/**
97+
* Navigates to the next tip in the catalog without permanently dismissing the current one.
98+
* @param contextKeyService The context key service to evaluate tip eligibility.
99+
*/
100+
navigateToNextTip(contextKeyService: IContextKeyService): IChatTip | undefined;
101+
102+
/**
103+
* Navigates to the previous tip in the catalog without permanently dismissing the current one.
104+
* @param contextKeyService The context key service to evaluate tip eligibility.
105+
*/
106+
navigateToPreviousTip(contextKeyService: IContextKeyService): IChatTip | undefined;
79107
}
80108

81109
export interface ITipDefinition {
@@ -472,6 +500,12 @@ export class ChatTipService extends Disposable implements IChatTipService {
472500
private readonly _onDidDismissTip = this._register(new Emitter<void>());
473501
readonly onDidDismissTip = this._onDidDismissTip.event;
474502

503+
private readonly _onDidNavigateTip = this._register(new Emitter<IChatTip>());
504+
readonly onDidNavigateTip = this._onDidNavigateTip.event;
505+
506+
private readonly _onDidHideTip = this._register(new Emitter<void>());
507+
readonly onDidHideTip = this._onDidHideTip.event;
508+
475509
private readonly _onDidDisableTips = this._register(new Emitter<void>());
476510
readonly onDidDisableTips = this._onDidDisableTips.event;
477511

@@ -557,6 +591,13 @@ export class ChatTipService extends Disposable implements IChatTipService {
557591
}
558592
}
559593

594+
hideTip(): void {
595+
this._hasShownRequestTip = false;
596+
this._shownTip = undefined;
597+
this._tipRequestId = undefined;
598+
this._onDidHideTip.fire();
599+
}
600+
560601
async disableTips(): Promise<void> {
561602
this._hasShownRequestTip = false;
562603
this._shownTip = undefined;
@@ -688,6 +729,40 @@ export class ChatTipService extends Disposable implements IChatTipService {
688729
return this._createTip(selectedTip);
689730
}
690731

732+
navigateToNextTip(contextKeyService: IContextKeyService): IChatTip | undefined {
733+
return this._navigateTip(1, contextKeyService);
734+
}
735+
736+
navigateToPreviousTip(contextKeyService: IContextKeyService): IChatTip | undefined {
737+
return this._navigateTip(-1, contextKeyService);
738+
}
739+
740+
private _navigateTip(direction: 1 | -1, contextKeyService: IContextKeyService): IChatTip | undefined {
741+
if (!this._shownTip) {
742+
return undefined;
743+
}
744+
745+
const currentIndex = TIP_CATALOG.findIndex(t => t.id === this._shownTip!.id);
746+
if (currentIndex === -1) {
747+
return undefined;
748+
}
749+
750+
const dismissedIds = new Set(this._getDismissedTipIds());
751+
for (let i = 1; i < TIP_CATALOG.length; i++) {
752+
const idx = ((currentIndex + direction * i) % TIP_CATALOG.length + TIP_CATALOG.length) % TIP_CATALOG.length;
753+
const candidate = TIP_CATALOG[idx];
754+
if (!dismissedIds.has(candidate.id) && this._isEligible(candidate, contextKeyService)) {
755+
this._shownTip = candidate;
756+
this._storageService.store(ChatTipService._LAST_TIP_ID_KEY, candidate.id, StorageScope.PROFILE, StorageTarget.USER);
757+
const tip = this._createTip(candidate);
758+
this._onDidNavigateTip.fire(tip);
759+
return tip;
760+
}
761+
}
762+
763+
return undefined;
764+
}
765+
691766
private _isEligible(tip: ITipDefinition, contextKeyService: IContextKeyService): boolean {
692767
if (tip.when && !contextKeyService.contextMatchesRules(tip.when)) {
693768
this._logService.debug('#ChatTips: tip is not eligible due to when clause', tip.id, tip.when.serialize());

src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTipContentPart.ts

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import { Emitter } from '../../../../../../base/common/event.js';
1313
import { Disposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js';
1414
import { localize, localize2 } from '../../../../../../nls.js';
1515
import { getFlatContextMenuActions } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js';
16+
import { MenuWorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js';
1617
import { Action2, IMenuService, MenuId, registerAction2 } from '../../../../../../platform/actions/common/actions.js';
1718
import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js';
1819
import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js';
19-
import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js';
20+
import { IInstantiationService, ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js';
2021
import { IMarkdownRenderer } from '../../../../../../platform/markdown/browser/markdownRenderer.js';
2122
import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js';
2223
import { IChatTip, IChatTipService } from '../../chatTipService.js';
@@ -30,6 +31,7 @@ export class ChatTipContentPart extends Disposable {
3031
public readonly onDidHide = this._onDidHide.event;
3132

3233
private readonly _renderedContent = this._register(new MutableDisposable());
34+
private readonly _toolbar = this._register(new MutableDisposable<MenuWorkbenchToolBar>());
3335

3436
private readonly _inChatTipContextKey: IContextKey<boolean>;
3537

@@ -41,6 +43,7 @@ export class ChatTipContentPart extends Disposable {
4143
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
4244
@IMenuService private readonly _menuService: IMenuService,
4345
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
46+
@IInstantiationService private readonly _instantiationService: IInstantiationService,
4447
) {
4548
super();
4649

@@ -66,6 +69,14 @@ export class ChatTipContentPart extends Disposable {
6669
}
6770
}));
6871

72+
this._register(this._chatTipService.onDidNavigateTip(tip => {
73+
this._renderTip(tip);
74+
}));
75+
76+
this._register(this._chatTipService.onDidHideTip(() => {
77+
this._onDidHide.fire();
78+
}));
79+
6980
this._register(this._chatTipService.onDidDisableTips(() => {
7081
this._onDidHide.fire();
7182
}));
@@ -93,10 +104,22 @@ export class ChatTipContentPart extends Disposable {
93104

94105
private _renderTip(tip: IChatTip): void {
95106
dom.clearNode(this.domNode);
107+
this._toolbar.clear();
108+
96109
this.domNode.appendChild(renderIcon(Codicon.lightbulb));
97110
const markdownContent = this._renderer.render(tip.content);
98111
this._renderedContent.value = markdownContent;
99112
this.domNode.appendChild(markdownContent.element);
113+
114+
// Toolbar with previous, next, and dismiss actions via MenuWorkbenchToolBar
115+
const toolbarContainer = $('.chat-tip-toolbar');
116+
this._toolbar.value = this._instantiationService.createInstance(MenuWorkbenchToolBar, toolbarContainer, MenuId.ChatTipToolbar, {
117+
menuOptions: {
118+
shouldForwardArgs: true,
119+
},
120+
});
121+
this.domNode.appendChild(toolbarContainer);
122+
100123
const textContent = markdownContent.element.textContent ?? localize('chatTip', "Chat tip");
101124
const hasLink = /\[.*?\]\(.*?\)/.test(tip.content.value);
102125
const ariaLabel = hasLink
@@ -107,6 +130,94 @@ export class ChatTipContentPart extends Disposable {
107130
}
108131
}
109132

133+
//#region Tip toolbar actions
134+
135+
registerAction2(class PreviousTipAction extends Action2 {
136+
constructor() {
137+
super({
138+
id: 'workbench.action.chat.previousTip',
139+
title: localize2('chatTip.previous', "Previous Tip"),
140+
icon: Codicon.chevronLeft,
141+
f1: false,
142+
menu: [{
143+
id: MenuId.ChatTipToolbar,
144+
group: 'navigation',
145+
order: 1,
146+
}]
147+
});
148+
}
149+
150+
override async run(accessor: ServicesAccessor): Promise<void> {
151+
const chatTipService = accessor.get(IChatTipService);
152+
const contextKeyService = accessor.get(IContextKeyService);
153+
chatTipService.navigateToPreviousTip(contextKeyService);
154+
}
155+
});
156+
157+
registerAction2(class NextTipAction extends Action2 {
158+
constructor() {
159+
super({
160+
id: 'workbench.action.chat.nextTip',
161+
title: localize2('chatTip.next', "Next Tip"),
162+
icon: Codicon.chevronRight,
163+
f1: false,
164+
menu: [{
165+
id: MenuId.ChatTipToolbar,
166+
group: 'navigation',
167+
order: 2,
168+
}]
169+
});
170+
}
171+
172+
override async run(accessor: ServicesAccessor): Promise<void> {
173+
const chatTipService = accessor.get(IChatTipService);
174+
const contextKeyService = accessor.get(IContextKeyService);
175+
chatTipService.navigateToNextTip(contextKeyService);
176+
}
177+
});
178+
179+
registerAction2(class DismissTipToolbarAction extends Action2 {
180+
constructor() {
181+
super({
182+
id: 'workbench.action.chat.dismissTipToolbar',
183+
title: localize2('chatTip.dismissButton', "Dismiss Tip"),
184+
icon: Codicon.check,
185+
f1: false,
186+
menu: [{
187+
id: MenuId.ChatTipToolbar,
188+
group: 'navigation',
189+
order: 3,
190+
}]
191+
});
192+
}
193+
194+
override async run(accessor: ServicesAccessor): Promise<void> {
195+
accessor.get(IChatTipService).dismissTip();
196+
}
197+
});
198+
199+
registerAction2(class CloseTipToolbarAction extends Action2 {
200+
constructor() {
201+
super({
202+
id: 'workbench.action.chat.closeTip',
203+
title: localize2('chatTip.close', "Close Tips"),
204+
icon: Codicon.close,
205+
f1: false,
206+
menu: [{
207+
id: MenuId.ChatTipToolbar,
208+
group: 'navigation',
209+
order: 4,
210+
}]
211+
});
212+
}
213+
214+
override async run(accessor: ServicesAccessor): Promise<void> {
215+
accessor.get(IChatTipService).hideTip();
216+
}
217+
});
218+
219+
//#endregion
220+
110221
//#region Tip context menu actions
111222

112223
registerAction2(class DismissTipAction extends Action2 {

src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatTipContent.css

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
font-family: var(--vscode-chat-font-family, inherit);
1717
color: var(--vscode-descriptionForeground);
1818
position: relative;
19-
overflow: hidden;
2019
}
2120

2221
.interactive-item-container .chat-tip-widget .codicon-lightbulb {
@@ -37,6 +36,52 @@
3736
margin: 0;
3837
}
3938

39+
.interactive-item-container .chat-tip-widget .chat-tip-toolbar,
40+
.chat-getting-started-tip-container .chat-tip-widget .chat-tip-toolbar {
41+
opacity: 0;
42+
pointer-events: none;
43+
}
44+
45+
.interactive-item-container .chat-tip-widget .chat-tip-toolbar > .monaco-toolbar,
46+
.chat-getting-started-tip-container .chat-tip-widget .chat-tip-toolbar > .monaco-toolbar {
47+
position: absolute;
48+
top: -15px;
49+
right: 10px;
50+
height: 26px;
51+
line-height: 26px;
52+
background-color: var(--vscode-editorWidget-background);
53+
border: 1px solid var(--vscode-chat-requestBorder);
54+
border-radius: var(--vscode-cornerRadius-medium);
55+
z-index: 100;
56+
transition: opacity 0.1s ease-in-out;
57+
}
58+
59+
.interactive-item-container .chat-tip-widget:hover .chat-tip-toolbar,
60+
.interactive-item-container .chat-tip-widget:focus-within .chat-tip-toolbar,
61+
.chat-getting-started-tip-container .chat-tip-widget:hover .chat-tip-toolbar,
62+
.chat-getting-started-tip-container .chat-tip-widget:focus-within .chat-tip-toolbar {
63+
opacity: 1;
64+
pointer-events: auto;
65+
}
66+
67+
.interactive-item-container .chat-tip-widget .chat-tip-toolbar .action-item,
68+
.chat-getting-started-tip-container .chat-tip-widget .chat-tip-toolbar .action-item {
69+
height: 24px;
70+
width: 24px;
71+
margin: 1px 2px;
72+
}
73+
74+
.interactive-item-container .chat-tip-widget .chat-tip-toolbar .action-item .action-label,
75+
.chat-getting-started-tip-container .chat-tip-widget .chat-tip-toolbar .action-item .action-label {
76+
color: var(--vscode-descriptionForeground);
77+
}
78+
79+
.interactive-item-container .chat-tip-widget .chat-tip-toolbar .action-item .action-label:hover,
80+
.chat-getting-started-tip-container .chat-tip-widget .chat-tip-toolbar .action-item .action-label:hover {
81+
background-color: var(--vscode-toolbar-hoverBackground);
82+
color: var(--vscode-foreground);
83+
}
84+
4085
.chat-getting-started-tip-container {
4186
margin-bottom: -4px; /* Counter the flex gap */
4287
width: 100%;
@@ -57,7 +102,7 @@
57102
font-size: var(--vscode-chat-font-size-body-s);
58103
font-family: var(--vscode-chat-font-family, inherit);
59104
color: var(--vscode-descriptionForeground);
60-
overflow: hidden;
105+
position: relative;
61106
}
62107

63108
.chat-getting-started-tip-container .chat-tip-widget a {

0 commit comments

Comments
 (0)