diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index f0be1a9ea91d7..f8dcaac522a5f 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -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", diff --git a/src/vs/sessions/browser/media/style.css b/src/vs/sessions/browser/media/style.css index 1d4c768c0b18e..53f46f7cf228f 100644 --- a/src/vs/sessions/browser/media/style.css +++ b/src/vs/sessions/browser/media/style.css @@ -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 { diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index e76d44b8645d2..2774b967bfa67 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -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', diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatProgressContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatProgressContentPart.ts index dacfc4d1184a8..ac8bcdf076c83 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatProgressContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatProgressContentPart.ts @@ -186,7 +186,8 @@ export class ChatWorkingProgressContentPart extends Disposable implements IChatC ) { super(); this.explicitContent = workingProgress.content; - const persistentProgressEnabled = configurationService.getValue(ChatConfiguration.ChatPersistentProgressEnabled) !== false; + const persistentProgressEnabled = configurationService.getValue(ChatConfiguration.ChatPersistentProgressEnabled) !== false + && configurationService.getValue(ChatConfiguration.ProgressBorder) !== true; if (persistentProgressEnabled) { const pool = buildPhrasePool(defaultThinkingMessages, configurationService); this.label = pool[Math.floor(Math.random() * pool.length)]; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts index 05a004c927299..a4ac737f62398 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatThinkingContentPart.ts @@ -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(ChatConfiguration.ChatPersistentProgressEnabled) !== false; + this.showProgressDetails = this.configurationService.getValue(ChatConfiguration.ChatPersistentProgressEnabled) !== false + && this.configurationService.getValue(ChatConfiguration.ProgressBorder) !== true; const configuredMode = this.configurationService.getValue('chat.agent.thinkingStyle') ?? ThinkingDisplayMode.Collapsed; this.fixedScrollingMode = configuredMode === ThinkingDisplayMode.FixedScrolling; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index b9d1f1d95a35e..4a1c20895e239 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -1061,7 +1061,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer(ChatConfiguration.ChatPersistentProgressEnabled) !== false; + const showProgressDetails = this.configService.getValue(ChatConfiguration.ChatPersistentProgressEnabled) !== false + && this.configService.getValue(ChatConfiguration.ProgressBorder) !== true; if (element.isComplete) { return undefined; } @@ -1227,7 +1228,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer(ChatConfiguration.ChatPersistentProgressEnabled) !== false) { + if (element.isComplete && this.configService.getValue(ChatConfiguration.ChatPersistentProgressEnabled) !== false && this.configService.getValue(ChatConfiguration.ProgressBorder) !== true) { return; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 20951ea08e619..ea235a6c13645 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -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(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); diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 4b777d2711c02..05f8a04272fa6 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -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: ''; + 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 { diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 49a79cf797d9e..f26809501a5f4 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -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', diff --git a/src/vs/workbench/contrib/chat/common/widget/chatColors.ts b/src/vs/workbench/contrib/chat/common/widget/chatColors.ts index bb256ebba1988..7b56d9af30699 100644 --- a/src/vs/workbench/contrib/chat/common/widget/chatColors.ts +++ b/src/vs/workbench/contrib/chat/common/widget/chatColors.ts @@ -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);