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