trying new empty state! (#253689)

* exp welcome view

* use exp setting

* use exp setting

* positioned disclaimers under title to avoid overlap

* Updated chat placeholder text

* some fixes

* fix hygiene

* make sure to add additional message

* swap exp is active

* add new setting

---------

Co-authored-by: Elijah King <elijahwilliamking@icloud.com>
This commit is contained in:
Justin Chen
2025-07-10 16:49:51 +00:00
committed by GitHub
co-authored by Elijah King
parent 4bd42b349f
commit 73fbb0cccd
6 changed files with 72 additions and 18 deletions
@@ -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."),
@@ -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<boolean>('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
};
}
@@ -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<boolean>('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: {
@@ -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;
@@ -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<boolean>('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 {
@@ -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 {