diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 95b5d248e1cb..fa11055a422c 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -262,6 +262,12 @@ configurationRegistry.registerConfiguration({ default: 'inline', tags: ['experimental', 'onExp'], }, + 'chat.emptyChatState.enabled': { + type: 'boolean', + default: true, + description: nls.localize('chat.emptyChatState', "Shows a modified empty chat state with hints in the input placeholder text."), + tags: ['experimental', 'onExp'], + }, [mcpEnabledSection]: { type: 'boolean', description: nls.localize('chat.mcp.enabled', "Enables integration with Model Context Protocol servers to provide additional tools and functionality."), diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index dee436564927..b49bc87252dd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -751,12 +751,16 @@ export class ChatWidget extends Disposable implements IChatWidget { const configuration = this.configurationService.inspect('workbench.secondarySideBar.defaultVisibility'); const expIsActive = configuration.defaultValue !== 'hidden'; + const expEmptyState = this.configurationService.getValue('chat.emptyChatState.enabled'); + const chatSetupTriggerContext = ContextKeyExpr.or( ChatContextKeys.Setup.installed.negate(), ChatContextKeys.Entitlement.canSignUp ); let welcomeContent: IChatViewWelcomeContent; + const defaultAgent = this.chatAgentService.getDefaultAgent(this.location, this.input.currentModeKind); + const additionalMessage = defaultAgent?.metadata.additionalWelcomeMessage; if ((startupExpValue === StartupExperimentGroup.MaximizedChat || startupExpValue === StartupExperimentGroup.SplitEmptyEditorChat || startupExpValue === StartupExperimentGroup.SplitWelcomeChat @@ -764,9 +768,10 @@ export class ChatWidget extends Disposable implements IChatWidget { welcomeContent = this.getExpWelcomeViewContent(); this.container.classList.add('experimental-welcome-view'); } + else if (expEmptyState) { + welcomeContent = this.getWelcomeViewContent(additionalMessage, expEmptyState); + } else { - const defaultAgent = this.chatAgentService.getDefaultAgent(this.location, this.input.currentModeKind); - const additionalMessage = defaultAgent?.metadata.additionalWelcomeMessage; const tips = this.input.currentModeKind === ChatModeKind.Ask ? new MarkdownString(localize('chatWidget.tips', "{0} or type {1} to attach context\n\n{2} to chat with extensions\n\nType {3} to use commands", '$(attach)', '#', '$(mention)', '/'), { supportThemeIcons: true }) : new MarkdownString(localize('chatWidget.tips.withoutParticipants', "{0} or type {1} to attach context", '$(attach)', '#'), { supportThemeIcons: true }); @@ -790,27 +795,37 @@ export class ChatWidget extends Disposable implements IChatWidget { } } - private getWelcomeViewContent(additionalMessage: string | IMarkdownString | undefined): IChatViewWelcomeContent { - const baseMessage = localize('chatMessage', "Copilot is powered by AI, so mistakes are possible. Review output carefully before use."); + private getWelcomeViewContent(additionalMessage: string | IMarkdownString | undefined, expEmptyState?: boolean): IChatViewWelcomeContent { + const disclaimerMessage = expEmptyState + ? localize('chatDisclaimer', "AI responses may be inaccurate.") + : localize('chatMessage', "Copilot is powered by AI, so mistakes are possible. Review output carefully before use."); + const icon = expEmptyState ? Codicon.chatSparkle : Codicon.copilotLarge; + if (this.input.currentModeKind === ChatModeKind.Ask) { return { - title: localize('chatDescription', "Ask Copilot"), - message: new MarkdownString(baseMessage), - icon: Codicon.copilotLarge, + title: localize('chatDescription', "Ask about your code."), + message: new MarkdownString(disclaimerMessage), + icon, additionalMessage, }; } else if (this.input.currentModeKind === ChatModeKind.Edit) { + const editsHelpMessage = localize('editsHelp', "Start your editing session by defining a set of files that you want to work with. Then ask Copilot for the changes you want to make."); + const message = expEmptyState ? disclaimerMessage : `${editsHelpMessage}\n\n${disclaimerMessage}`; + return { - title: localize('editsTitle', "Edit with Copilot"), - message: new MarkdownString(localize('editsMessage', "Start your editing session by defining a set of files that you want to work with. Then ask Copilot for the changes you want to make.") + `\n\n${baseMessage}`), - icon: Codicon.copilotLarge, + title: localize('editsTitle', "Edit in context."), + message: new MarkdownString(message), + icon, additionalMessage }; } else { + const agentHelpMessage = localize('agentMessage', "Ask Copilot to edit your files in [agent mode]({0}). Copilot will automatically use multiple requests to pick files to edit, run terminal commands, and iterate on errors.", 'https://aka.ms/vscode-copilot-agent'); + const message = expEmptyState ? disclaimerMessage : `${agentHelpMessage}\n\n${disclaimerMessage}`; + return { - title: localize('editsTitle', "Edit with Copilot"), - message: new MarkdownString(localize('agentMessage', "Ask Copilot to edit your files in [agent mode]({0}). Copilot will automatically use multiple requests to pick files to edit, run terminal commands, and iterate on errors.", 'https://aka.ms/vscode-copilot-agent') + `\n\n${baseMessage}`), - icon: Codicon.copilotLarge, + title: localize('agentTitle', "Build with agent mode."), + message: new MarkdownString(message), + icon, additionalMessage }; } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index cf529794f3a3..f61e7d6b3e90 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -11,6 +11,8 @@ import { ICodeEditorService } from '../../../../../editor/browser/services/codeE import { Range } from '../../../../../editor/common/core/range.js'; import { IDecorationOptions } from '../../../../../editor/common/editorCommon.js'; import { TrackedRangeStickiness } from '../../../../../editor/common/model.js'; +import { localize } from '../../../../../nls.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { inputPlaceholderForeground } from '../../../../../platform/theme/common/colorRegistry.js'; import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; @@ -18,6 +20,7 @@ import { IChatAgentCommand, IChatAgentData, IChatAgentService } from '../../comm import { chatSlashCommandBackground, chatSlashCommandForeground } from '../../common/chatColors.js'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, ChatRequestToolSetPart, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader } from '../../common/chatParserTypes.js'; import { ChatRequestParser } from '../../common/chatRequestParser.js'; +import { ChatModeKind } from '../../common/constants.js'; import { IChatWidget } from '../chat.js'; import { ChatWidget } from '../chatWidget.js'; import { dynamicVariableDecorationType } from './chatDynamicVariables.js'; @@ -44,6 +47,7 @@ class InputEditorDecorations extends Disposable { @ICodeEditorService private readonly codeEditorService: ICodeEditorService, @IThemeService private readonly themeService: IThemeService, @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); @@ -126,7 +130,16 @@ class InputEditorDecorations extends Disposable { } if (!inputValue) { - const description = this.widget.input.currentModeObs.get().description.get(); + const mode = this.widget.input.currentModeObs.get(); + let description = mode.description.get(); + if (this.configurationService.getValue('chat.emptyChatState.enabled')) { + if (mode.kind === ChatModeKind.Ask) { + description += ` ${localize('askPlaceholderHint', "# context, @ extensions, / commands")}`; + } else if (mode.kind === ChatModeKind.Edit || mode.kind === ChatModeKind.Agent) { + description += ` ${localize('editPlaceholderHint', "# context")}`; + } + } + const decoration: IDecorationOptions[] = [ { range: { diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css index d6d06e188c0f..6523d2807b4d 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css @@ -80,6 +80,22 @@ div.chat-welcome-view { } } + & > .chat-welcome-view-message.experimental-empty-state { + position: relative; + text-align: center; + max-width: 100%; + margin: 0 auto; + color: var(--vscode-input-placeholderForeground); + + a { + color: var(--vscode-textLink-foreground); + } + p{ + margin-top: 8px; + margin-bottom: 8px; + } + } + .monaco-button { display: inline-block; width: initial; diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts index 603baeeb982b..46d8ccadeeac 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts @@ -20,6 +20,7 @@ import { ITelemetryService } from '../../../../../platform/telemetry/common/tele import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; import { ChatAgentLocation } from '../../common/constants.js'; import { IChatWidgetService } from '../chat.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { chatViewsWelcomeRegistry, IChatViewsWelcomeDescriptor } from './chatViewsWelcome.js'; const $ = dom.$; @@ -142,6 +143,7 @@ export class ChatViewWelcomePart extends Disposable { @ILogService private logService: ILogService, @IChatWidgetService private chatWidgetService: IChatWidgetService, @ITelemetryService private telemetryService: ITelemetryService, + @IConfigurationService private configurationService: IConfigurationService, ) { super(); this.element = dom.$('.chat-welcome-view'); @@ -160,13 +162,15 @@ export class ChatViewWelcomePart extends Disposable { title.textContent = content.title; // Preview indicator - if (typeof content.message !== 'function' && options?.isWidgetAgentWelcomeViewContent) { + const expEmptyState = this.configurationService.getValue('chat.emptyChatState.enabled'); + if (typeof content.message !== 'function' && options?.isWidgetAgentWelcomeViewContent && !expEmptyState) { const container = dom.append(this.element, $('.chat-welcome-view-indicator-container')); dom.append(container, $('.chat-welcome-view-subtitle', undefined, localize('agentModeSubtitle', "Agent Mode"))); } // Message const message = dom.append(this.element, content.isExperimental ? $('.chat-welcome-experimental-view-message') : $('.chat-welcome-view-message')); + message.classList.toggle('experimental-empty-state', expEmptyState); if (typeof content.message === 'function') { dom.append(message, content.message(this._register(new DisposableStore()))); } else { diff --git a/src/vs/workbench/contrib/chat/common/chatModes.ts b/src/vs/workbench/contrib/chat/common/chatModes.ts index f805a65d42e6..85d0bbb9750d 100644 --- a/src/vs/workbench/contrib/chat/common/chatModes.ts +++ b/src/vs/workbench/contrib/chat/common/chatModes.ts @@ -333,9 +333,9 @@ export class BuiltinChatMode implements IChatMode { } export namespace ChatMode { - export const Ask = new BuiltinChatMode(ChatModeKind.Ask, 'Ask', localize('chatDescription', "Ask Copilot")); - export const Edit = new BuiltinChatMode(ChatModeKind.Edit, 'Edit', localize('editsDescription', "Edit files in your workspace")); - export const Agent = new BuiltinChatMode(ChatModeKind.Agent, 'Agent', localize('agentDescription', "Edit files in your workspace in agent mode")); + export const Ask = new BuiltinChatMode(ChatModeKind.Ask, 'Ask', localize('chatDescription', "Ask a question.")); + export const Edit = new BuiltinChatMode(ChatModeKind.Edit, 'Edit', localize('editsDescription', "Edit files.")); + export const Agent = new BuiltinChatMode(ChatModeKind.Agent, 'Agent', localize('agentDescription', "Build autonomously.")); } export function isBuiltinChatMode(mode: IChatMode): boolean {