Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationWelcomePagePromptLaunchers.ts
13406 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import './media/aiCustomizationWelcomePromptLaunchers.css';
7
import * as DOM from '../../../../../base/browser/dom.js';
8
import { DomScrollableElement } from '../../../../../base/browser/ui/scrollbar/scrollableElement.js';
9
import { ScrollbarVisibility } from '../../../../../base/common/scrollable.js';
10
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
11
import { localize } from '../../../../../nls.js';
12
import { ThemeIcon } from '../../../../../base/common/themables.js';
13
import { Codicon } from '../../../../../base/common/codicons.js';
14
import type { ICommandService } from '../../../../../platform/commands/common/commands.js';
15
import { AICustomizationManagementSection } from './aiCustomizationManagement.js';
16
import { agentIcon, instructionsIcon, pluginIcon, skillIcon, hookIcon } from './aiCustomizationIcons.js';
17
import { IAICustomizationWorkspaceService, IWelcomePageFeatures } from '../../common/aiCustomizationWorkspaceService.js';
18
import { PromptsType } from '../../common/promptSyntax/promptTypes.js';
19
import type { IAICustomizationWelcomePageImplementation, IWelcomePageCallbacks } from './aiCustomizationWelcomePage.js';
20
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
21
import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js';
22
23
const $ = DOM.$;
24
25
interface IPromptLaunchersCategoryDescription {
26
readonly id: AICustomizationManagementSection;
27
readonly label: string;
28
readonly icon: ThemeIcon;
29
readonly description: string;
30
readonly promptType?: PromptsType;
31
}
32
33
export class PromptLaunchersAICustomizationWelcomePage extends Disposable implements IAICustomizationWelcomePageImplementation {
34
35
private readonly cardDisposables = this._register(new DisposableStore());
36
37
readonly container: HTMLElement;
38
private readonly scrollable: DomScrollableElement;
39
private cardsContainer: HTMLElement | undefined;
40
private inputElement: HTMLInputElement | undefined;
41
42
private sentLabel: HTMLElement | undefined;
43
private submitBtn: HTMLElement | undefined;
44
private inputRow: HTMLElement | undefined;
45
46
private readonly categoryDescriptions: IPromptLaunchersCategoryDescription[] = [
47
{
48
id: AICustomizationManagementSection.Agents,
49
label: localize('agents', "Agents"),
50
icon: agentIcon,
51
description: localize('agentsDesc', "Define custom agents with specialized personas, tool access, and instructions for specific tasks."),
52
promptType: PromptsType.agent,
53
},
54
{
55
id: AICustomizationManagementSection.Skills,
56
label: localize('skills', "Skills"),
57
icon: skillIcon,
58
description: localize('skillsDesc', "Create reusable skill files that provide domain-specific knowledge and workflows."),
59
promptType: PromptsType.skill,
60
},
61
{
62
id: AICustomizationManagementSection.Instructions,
63
label: localize('instructions', "Instructions"),
64
icon: instructionsIcon,
65
description: localize('instructionsDesc', "Set always-on instructions that guide AI behavior across your workspace or user profile."),
66
promptType: PromptsType.instructions,
67
},
68
{
69
id: AICustomizationManagementSection.Hooks,
70
label: localize('hooks', "Hooks"),
71
icon: hookIcon,
72
description: localize('hooksDesc', "Configure automated actions triggered by events like saving files or running tasks."),
73
promptType: PromptsType.hook,
74
},
75
{
76
id: AICustomizationManagementSection.McpServers,
77
label: localize('mcpServers', "MCP Servers"),
78
icon: Codicon.server,
79
description: localize('mcpServersDesc', "Connect external tool servers that extend AI capabilities with custom tools and data sources."),
80
},
81
{
82
id: AICustomizationManagementSection.Plugins,
83
label: localize('plugins', "Plugins"),
84
icon: pluginIcon,
85
description: localize('pluginsDesc', "Install and manage agent plugins that add additional tools, skills, and integrations."),
86
},
87
];
88
89
constructor(
90
parent: HTMLElement,
91
private readonly welcomePageFeatures: IWelcomePageFeatures | undefined,
92
private readonly callbacks: IWelcomePageCallbacks,
93
_commandService: ICommandService,
94
private readonly workspaceService: IAICustomizationWorkspaceService,
95
private readonly hoverService: IHoverService,
96
) {
97
super();
98
99
this.container = $('.welcome-prompts-content-container');
100
this.scrollable = this._register(new DomScrollableElement(this.container, {
101
horizontal: ScrollbarVisibility.Hidden,
102
vertical: ScrollbarVisibility.Auto,
103
useShadows: false,
104
}));
105
const scrollableNode = this.scrollable.getDomNode();
106
scrollableNode.classList.add('welcome-prompts-scrollable');
107
parent.appendChild(scrollableNode);
108
109
// Re-scan whenever the wrapper changes size so the scrollbar reflects
110
// the current overflow state. rebuildCards() scans after content changes.
111
const resizeObserver = this._register(new DOM.DisposableResizeObserver(() => this.scrollable.scanDomNode()));
112
this._register(resizeObserver.observe(scrollableNode));
113
114
const welcomeInner = DOM.append(this.container, $('.welcome-prompts-inner'));
115
116
const heading = DOM.append(welcomeInner, $('h2.welcome-prompts-heading'));
117
heading.textContent = localize('welcomeHeading', "Agent Customizations");
118
119
const subtitle = DOM.append(welcomeInner, $('p.welcome-prompts-subtitle'));
120
subtitle.textContent = localize('welcomeSubtitle', "Tailor how agents work in your projects. Configure workspace customizations for the entire team, or create personal ones that follow you across projects.");
121
122
if (this.welcomePageFeatures?.showGettingStartedBanner !== false) {
123
const gettingStarted = DOM.append(welcomeInner, $('.welcome-prompts-primary'));
124
const header = DOM.append(gettingStarted, $('.welcome-prompts-section-label'));
125
const icon = DOM.append(header, $('span.welcome-prompts-section-label-icon.codicon.codicon-sparkle'));
126
icon.setAttribute('aria-hidden', 'true');
127
const title = DOM.append(header, $('span'));
128
title.textContent = localize('gettingStartedTitle', "Customize Your Agent");
129
130
const description = DOM.append(gettingStarted, $('p.welcome-prompts-input-helper'));
131
description.textContent = localize('gettingStartedDesc', "Describe your preferences and conventions to draft agents, skills, and instructions.");
132
133
const inputRow = DOM.append(gettingStarted, $('.welcome-prompts-input-row'));
134
this.inputRow = inputRow;
135
this.inputElement = DOM.append(inputRow, $('input.welcome-prompts-input')) as HTMLInputElement;
136
this.inputElement.type = 'text';
137
this.inputElement.placeholder = localize('workflowInputPlaceholder', "Prefer concise commits, thorough reviews, and tested code...");
138
this.inputElement.setAttribute('aria-label', localize('workflowInputAriaLabel', "Describe your preferences to customize your agent"));
139
140
const submitBtn = DOM.append(inputRow, $('button.welcome-prompts-input-submit'));
141
this.submitBtn = submitBtn;
142
submitBtn.setAttribute('aria-label', localize('workflowSubmitAriaLabel', "Customize agent"));
143
this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), submitBtn, localize('workflowSubmitTooltip', "Open in Chat")));
144
const chevron = DOM.append(submitBtn, $('span.codicon.codicon-arrow-up'));
145
chevron.setAttribute('aria-hidden', 'true');
146
147
const updateSubmitState = () => {
148
const hasValue = !!(this.inputElement?.value?.trim());
149
(submitBtn as HTMLButtonElement).disabled = !hasValue;
150
submitBtn.classList.toggle('welcome-prompts-input-submit-disabled', !hasValue);
151
};
152
153
const submit = () => {
154
const value = this.inputElement?.value?.trim();
155
if (!value) {
156
return;
157
}
158
let query: string;
159
if (this.workspaceService.isSessionsWindow) {
160
query = `Generate agent customizations. ${value}`;
161
} else {
162
query = `/init ${value}`;
163
}
164
165
// Show confirmation immediately — before prefillChat so it's visible
166
// even if prefillChat navigates focus away from this editor
167
if (this.inputElement) {
168
this.inputElement.value = '';
169
}
170
updateSubmitState();
171
inputRow.classList.add('sent');
172
submitBtn.style.display = 'none';
173
if (this.sentLabel) {
174
this.sentLabel.remove();
175
}
176
this.sentLabel = DOM.append(inputRow, $('span.welcome-prompts-sent-label'));
177
this.sentLabel.textContent = localize('sentToChat', "Sent to chat \u2713");
178
179
this.callbacks.prefillChat(query, { isPartialQuery: false, newChat: true });
180
};
181
182
this._register(DOM.addDisposableListener(submitBtn, 'click', e => { e.stopPropagation(); submit(); }));
183
this._register(DOM.addDisposableListener(this.inputElement, 'keydown', (e: KeyboardEvent) => {
184
if (e.key === 'Enter') {
185
e.preventDefault();
186
submit();
187
}
188
}));
189
this._register(DOM.addDisposableListener(this.inputElement, 'input', () => {
190
updateSubmitState();
191
// Typing restores the input row from sent state
192
this._clearSentState();
193
}));
194
updateSubmitState();
195
}
196
197
this.cardsContainer = DOM.append(welcomeInner, $('.welcome-prompts-cards'));
198
}
199
200
private _clearSentState(): void {
201
if (this.sentLabel) {
202
this.sentLabel.remove();
203
this.sentLabel = undefined;
204
}
205
if (this.submitBtn) {
206
this.submitBtn.style.display = '';
207
}
208
if (this.inputRow) {
209
this.inputRow.classList.remove('sent');
210
}
211
}
212
213
reset(): void {
214
this._clearSentState();
215
}
216
217
rebuildCards(visibleSectionIds: ReadonlySet<AICustomizationManagementSection>): void {
218
if (!this.cardsContainer) {
219
return;
220
}
221
222
this.cardDisposables.clear();
223
DOM.clearNode(this.cardsContainer);
224
225
for (const category of this.categoryDescriptions) {
226
if (!visibleSectionIds.has(category.id)) {
227
continue;
228
}
229
230
const card = DOM.append(this.cardsContainer, $('.welcome-prompts-card'));
231
card.setAttribute('tabindex', '0');
232
card.setAttribute('role', 'button');
233
234
const cardHeader = DOM.append(card, $('.welcome-prompts-card-header'));
235
const iconEl = DOM.append(cardHeader, $('.welcome-prompts-card-icon'));
236
iconEl.classList.add(...ThemeIcon.asClassNameArray(category.icon));
237
const labelEl = DOM.append(cardHeader, $('span.welcome-prompts-card-label'));
238
labelEl.textContent = category.label;
239
240
const descEl = DOM.append(card, $('p.welcome-prompts-card-description'));
241
descEl.textContent = category.description;
242
243
const footer = DOM.append(card, $('.welcome-prompts-card-footer'));
244
if (category.promptType) {
245
const generateBtn = DOM.append(footer, $('button.welcome-prompts-card-action'));
246
generateBtn.textContent = localize('new', "New...");
247
this.cardDisposables.add(DOM.addDisposableListener(generateBtn, 'click', e => {
248
e.stopPropagation();
249
this.callbacks.closeEditor();
250
if (this.workspaceService.isSessionsWindow) {
251
const typeLabel = category.label.toLowerCase().replace(/s$/, '');
252
this.callbacks.prefillChat(`Create me a custom ${typeLabel} that `, { isPartialQuery: true, newChat: true });
253
} else {
254
this.workspaceService.generateCustomization(category.promptType!);
255
}
256
}));
257
} else {
258
const browseBtn = DOM.append(footer, $('button.welcome-prompts-card-action'));
259
browseBtn.textContent = localize('browse', "Browse...");
260
this.cardDisposables.add(DOM.addDisposableListener(browseBtn, 'click', e => {
261
e.stopPropagation();
262
this.callbacks.selectSectionWithMarketplace(category.id);
263
}));
264
}
265
266
this.cardDisposables.add(DOM.addDisposableListener(card, 'click', () => {
267
this.callbacks.selectSection(category.id);
268
}));
269
this.cardDisposables.add(DOM.addDisposableListener(card, 'keydown', e => {
270
if (e.key === 'Enter' || e.key === ' ') {
271
e.preventDefault();
272
this.callbacks.selectSection(category.id);
273
}
274
}));
275
}
276
277
// Content changed — recompute scroll dimensions.
278
this.scrollable.scanDomNode();
279
}
280
281
focus(): void {
282
this.inputElement?.focus();
283
}
284
}
285
286