show animation around chat input while working (#311125)

* developer joy animation around input box

* add back removed stuff

* don't show border while stuff is working

* better css

* add color components and cleanup

* progress border
This commit is contained in:
Justin Chen
2026-04-21 07:07:40 +02:00
committed by GitHub
parent e8a7c6d6df
commit 1a39bedfe8
10 changed files with 129 additions and 5 deletions
@@ -61,6 +61,9 @@
"--vscode-chat-avatarForeground",
"--vscode-chat-checkpointSeparator",
"--vscode-chat-editedFileForeground",
"--vscode-chat-inputWorkingBorderColor1",
"--vscode-chat-inputWorkingBorderColor2",
"--vscode-chat-inputWorkingBorderColor3",
"--vscode-chat-linesAddedForeground",
"--vscode-chat-linesRemovedForeground",
"--vscode-chat-requestBackground",
@@ -1037,6 +1040,8 @@
"--monaco-editor-warning-decoration",
"--animation-angle",
"--animation-opacity",
"--chat-input-anim-angle",
"--chat-input-working-fill",
"--chat-setup-dialog-glow-angle",
"--vscode-chat-font-family",
"--vscode-chat-font-size-body-l",
+9
View File
@@ -387,12 +387,21 @@
border-color: var(--vscode-agentsChatInput-border) !important;
background-color: var(--vscode-agentsChatInput-background);
color: var(--vscode-agentsChatInput-foreground);
/* Preserve the agents-app input background under the developer-joy ring. */
--chat-input-working-fill: var(--vscode-agentsChatInput-background);
}
.agent-sessions-workbench .interactive-session .chat-input-container.focused {
border-color: var(--vscode-agentsChatInput-focusBorder, var(--vscode-focusBorder)) !important;
}
/* While the developer-joy animated border is active, suppress the static
border so it doesn't visually conflict with the spinning gradient ring. */
.agent-sessions-workbench .interactive-session .chat-input-container.working,
.agent-sessions-workbench .interactive-session .chat-input-container.working.focused {
border-color: transparent !important;
}
/* Make the Monaco editor inside the chat input transparent so it inherits the chatInput.background */
.agent-sessions-workbench .interactive-session .interactive-input-part .chat-editor-container .interactive-input-editor .monaco-editor,
.agent-sessions-workbench .interactive-session .interactive-input-part .chat-editor-container .interactive-input-editor .monaco-editor .monaco-editor-background {
@@ -638,7 +638,12 @@ configurationRegistry.registerConfiguration({
[ChatConfiguration.ChatPersistentProgressEnabled]: {
type: 'boolean',
default: product.quality !== 'stable',
description: nls.localize('chat.persistentProgress.enabled', "Show elapsed time and token usage in chat response progress."),
description: nls.localize('chat.persistentProgress.enabled', "Always show progress in chat."),
},
[ChatConfiguration.ProgressBorder]: {
type: 'boolean',
default: false,
markdownDescription: nls.localize('chat.progressBorder.enabled', "Show an animated gradient border around the chat input while the agent is working or thinking. When enabled, this overrides {0} to be off.", '`#chat.persistentProgress.enabled#`'),
},
[ChatConfiguration.NotifyWindowOnResponseReceived]: {
type: 'string',
@@ -186,7 +186,8 @@ export class ChatWorkingProgressContentPart extends Disposable implements IChatC
) {
super();
this.explicitContent = workingProgress.content;
const persistentProgressEnabled = configurationService.getValue<boolean>(ChatConfiguration.ChatPersistentProgressEnabled) !== false;
const persistentProgressEnabled = configurationService.getValue<boolean>(ChatConfiguration.ChatPersistentProgressEnabled) !== false
&& configurationService.getValue<boolean>(ChatConfiguration.ProgressBorder) !== true;
if (persistentProgressEnabled) {
const pool = buildPhrasePool(defaultThinkingMessages, configurationService);
this.label = pool[Math.floor(Math.random() * pool.length)];
@@ -304,7 +304,8 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
this.id = content.id;
this.content = content;
this.allThinkingParts.push(content);
this.showProgressDetails = this.configurationService.getValue<boolean>(ChatConfiguration.ChatPersistentProgressEnabled) !== false;
this.showProgressDetails = this.configurationService.getValue<boolean>(ChatConfiguration.ChatPersistentProgressEnabled) !== false
&& this.configurationService.getValue<boolean>(ChatConfiguration.ProgressBorder) !== true;
const configuredMode = this.configurationService.getValue<ThinkingDisplayMode>('chat.agent.thinkingStyle') ?? ThinkingDisplayMode.Collapsed;
this.fixedScrollingMode = configuredMode === ThinkingDisplayMode.FixedScrolling;
@@ -1061,7 +1061,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
return undefined;
}
const showProgressDetails = this.configService.getValue<boolean>(ChatConfiguration.ChatPersistentProgressEnabled) !== false;
const showProgressDetails = this.configService.getValue<boolean>(ChatConfiguration.ChatPersistentProgressEnabled) !== false
&& this.configService.getValue<boolean>(ChatConfiguration.ProgressBorder) !== true;
if (element.isComplete) {
return undefined;
}
@@ -1227,7 +1228,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
return;
}
if (element.isComplete && this.configService.getValue<boolean>(ChatConfiguration.ChatPersistentProgressEnabled) !== false) {
if (element.isComplete && this.configService.getValue<boolean>(ChatConfiguration.ChatPersistentProgressEnabled) !== false && this.configService.getValue<boolean>(ChatConfiguration.ProgressBorder) !== true) {
return;
}
@@ -465,6 +465,9 @@ export class ChatWidget extends Disposable implements IChatWidget {
this.updateChatViewVisibility();
}
}
if (e.affectsConfiguration(ChatConfiguration.ProgressBorder)) {
this.updateWorkingProgressBorder();
}
}));
this._register(bindContextKey(decidedChatEditingResourceContextKey, contextKeyService, (reader) => {
@@ -656,6 +659,20 @@ export class ChatWidget extends Disposable implements IChatWidget {
return this.inlineInputPartDisposable.value!;
}
private updateWorkingProgressBorder(): void {
const inputPart = this.inputPartDisposable.value;
if (!inputPart) {
return;
}
const inputContainer = inputPart.inputContainerElement;
if (!inputContainer) {
return;
}
const enabled = this.configurationService.getValue<boolean>(ChatConfiguration.ProgressBorder) === true;
const inProgress = !!this.viewModel?.model.requestInProgress.get();
inputContainer.classList.toggle('working', enabled && inProgress);
}
get inputEditor(): ICodeEditor {
return this.input.inputEditor;
}
@@ -1980,6 +1997,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
this.finishedEditing();
}
this.viewModel = undefined;
this.updateWorkingProgressBorder();
this.onDidChangeItems();
this._hasPendingRequestsContextKey.set(false);
return;
@@ -2035,6 +2053,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
this.requestInProgress.set(this.viewModel.model.requestInProgress.get());
this.hasActiveRequest.set(this.viewModel.model.hasActiveRequest.get());
this.updateWorkingProgressBorder();
// Update the editor's placeholder text when it changes in the view model
if (events?.some(e => e?.kind === 'changePlaceholder')) {
@@ -2053,6 +2072,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
}
// Disposes the viewmodel and listeners
this.viewModel = undefined;
this.updateWorkingProgressBorder();
this.onDidChangeItems();
}));
this._sessionIsEmptyContextKey.set(model.getRequests().length === 0);
@@ -871,6 +871,72 @@ have to be updated for changes to the rules above, or to support more deeply nes
overflow: hidden;
}
/* Animated gradient border shown around the chat input while the agent is
working or thinking. Toggled by the `chat.progressBorder.enabled`
setting and the chat widget's request-in-progress state. The ring is
rendered using layered `background-image` + `background-clip`
(`padding-box`/`border-box`), so it traces the input's outer corner
radius and isn't clipped by `overflow: hidden` on the parent. */
@property --chat-input-anim-angle {
syntax: '<angle>';
inherits: false;
initial-value: 135deg;
}
@keyframes chat-input-working-border-spin {
from {
--chat-input-anim-angle: 135deg;
}
to {
--chat-input-anim-angle: 495deg;
}
}
@keyframes chat-input-working-border-glow {
0%,
100% {
box-shadow:
0 0 4px 0 color-mix(in srgb, var(--vscode-chat-inputWorkingBorderColor1) 18%, transparent),
0 0 10px 0 color-mix(in srgb, var(--vscode-chat-inputWorkingBorderColor3) 8%, transparent);
}
50% {
box-shadow:
0 0 6px 0 color-mix(in srgb, var(--vscode-chat-inputWorkingBorderColor2) 22%, transparent),
0 0 14px 1px color-mix(in srgb, var(--vscode-chat-inputWorkingBorderColor1) 12%, transparent);
}
}
.monaco-workbench .interactive-session .chat-input-container.working {
border-color: transparent;
/* The padding-box layer fills the input interior. It defaults to the
standard input background, but each host can override
`--chat-input-working-fill` to keep its own background color (e.g. the
Sessions workbench sets it to `--vscode-agentsChatInput-background`).
The conic-gradient layer is clipped to the border box so it paints
exactly where the (transparent) border lives. */
background:
linear-gradient(var(--chat-input-working-fill, var(--vscode-input-background)),
var(--chat-input-working-fill, var(--vscode-input-background))) padding-box,
conic-gradient(from var(--chat-input-anim-angle),
var(--vscode-chat-inputWorkingBorderColor1),
var(--vscode-chat-inputWorkingBorderColor2),
var(--vscode-chat-inputWorkingBorderColor3),
var(--vscode-chat-inputWorkingBorderColor2),
var(--vscode-chat-inputWorkingBorderColor1)) border-box;
animation:
chat-input-working-border-spin 1.2s linear infinite,
chat-input-working-border-glow 3s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.monaco-workbench .interactive-session .chat-input-container.working {
animation: none;
}
}
/* Context usage widget container - positioned in the secondary toolbar below input */
.interactive-session .chat-input-toolbars .chat-context-usage-container,
.interactive-session .chat-secondary-toolbar .chat-context-usage-container {
@@ -48,6 +48,7 @@ export enum ChatConfiguration {
ChatViewProgressBadgeEnabled = 'chat.viewProgressBadge.enabled',
ChatContextUsageEnabled = 'chat.contextUsage.enabled',
ChatPersistentProgressEnabled = 'chat.persistentProgress.enabled',
ProgressBorder = 'chat.progressBorder.enabled',
SubagentToolCustomAgents = 'chat.customAgentInSubagent.enabled',
GeneralPurposeAgentEnabled = 'chat.generalPurposeAgent.enabled',
SubagentsAllowInvocationsFromSubagents = 'chat.subagents.allowInvocationsFromSubagents',
@@ -87,3 +87,18 @@ export const chatThinkingShimmer = registerColor(
'chat.thinkingShimmer',
{ dark: '#ffffff', light: '#000000', hcDark: '#ffffff', hcLight: '#000000' },
localize('chat.thinkingShimmer', 'Shimmer highlight for thinking/working labels.'), true);
export const chatInputWorkingBorderColor1 = registerColor(
'chat.inputWorkingBorderColor1',
{ dark: '#b44aff', light: '#b44aff', hcDark: '#b44aff', hcLight: '#b44aff' },
localize('chat.inputWorkingBorderColor1', 'First color stop of the animated chat input border shown while a request is in flight.'), true);
export const chatInputWorkingBorderColor2 = registerColor(
'chat.inputWorkingBorderColor2',
{ dark: '#4af0c0', light: '#4af0c0', hcDark: '#4af0c0', hcLight: '#4af0c0' },
localize('chat.inputWorkingBorderColor2', 'Second color stop of the animated chat input border shown while a request is in flight.'), true);
export const chatInputWorkingBorderColor3 = registerColor(
'chat.inputWorkingBorderColor3',
{ dark: '#51a2ff', light: '#51a2ff', hcDark: '#51a2ff', hcLight: '#51a2ff' },
localize('chat.inputWorkingBorderColor3', 'Third color stop of the animated chat input border shown while a request is in flight.'), true);