mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-14 23:18:36 +00:00
animations for reasoning and tool calls inside reasoning (#290216)
* animations for reasoning and tool calls inside reasoning * add shimmer * address some comments * Update src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * remove extra keyframes --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -66,6 +66,7 @@
|
||||
"--vscode-chat-requestCodeBorder",
|
||||
"--vscode-chat-slashCommandBackground",
|
||||
"--vscode-chat-slashCommandForeground",
|
||||
"--vscode-chat-thinkingShimmer",
|
||||
"--vscode-chatManagement-sashBorder",
|
||||
"--vscode-checkbox-background",
|
||||
"--vscode-checkbox-border",
|
||||
|
||||
@@ -108,6 +108,17 @@ interface ILazyThinkingItem {
|
||||
type ILazyItem = ILazyToolItem | ILazyThinkingItem;
|
||||
const THINKING_SCROLL_MAX_HEIGHT = 200;
|
||||
|
||||
const workingMessages = [
|
||||
localize('chat.thinking.working.1', 'Thinking...'),
|
||||
localize('chat.thinking.working.2', 'Processing...'),
|
||||
localize('chat.thinking.working.3', 'Analyzing...'),
|
||||
localize('chat.thinking.working.4', 'Computing...'),
|
||||
localize('chat.thinking.working.5', 'Loading...'),
|
||||
localize('chat.thinking.working.6', 'Reasoning...'),
|
||||
localize('chat.thinking.working.7', 'Evaluating...'),
|
||||
localize('chat.thinking.working.8', 'Preparing...'),
|
||||
];
|
||||
|
||||
export class ChatThinkingContentPart extends ChatCollapsibleContentPart implements IChatContentPart {
|
||||
public readonly codeblocks: undefined;
|
||||
public readonly codeblocksPartId: undefined;
|
||||
@@ -134,6 +145,17 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
private singleItemInfo: { element: HTMLElement; originalParent: HTMLElement; originalNextSibling: Node | null } | undefined;
|
||||
private lazyItems: ILazyItem[] = [];
|
||||
private hasExpandedOnce: boolean = false;
|
||||
private workingSpinnerElement: HTMLElement | undefined;
|
||||
private workingSpinnerLabel: HTMLElement | undefined;
|
||||
private availableWorkingMessages: string[] = [...workingMessages];
|
||||
|
||||
private getRandomWorkingMessage(): string {
|
||||
if (this.availableWorkingMessages.length === 0) {
|
||||
this.availableWorkingMessages = [...workingMessages];
|
||||
}
|
||||
const index = Math.floor(Math.random() * this.availableWorkingMessages.length);
|
||||
return this.availableWorkingMessages.splice(index, 1)[0];
|
||||
}
|
||||
|
||||
constructor(
|
||||
content: IChatThinkingPart,
|
||||
@@ -186,19 +208,20 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
if (this.fixedScrollingMode) {
|
||||
node.classList.add('chat-thinking-fixed-mode');
|
||||
this.currentTitle = this.defaultTitle;
|
||||
if (this._collapseButton && !this.element.isComplete) {
|
||||
this._collapseButton.icon = ThemeIcon.modify(Codicon.loading, 'spin');
|
||||
}
|
||||
}
|
||||
|
||||
// override for codicon chevron in the collapsible part
|
||||
this._register(autorun(r => {
|
||||
this.expanded.read(r);
|
||||
if (this._collapseButton && this.wrapper) {
|
||||
if (this.wrapper.classList.contains('chat-thinking-streaming') && !this.element.isComplete) {
|
||||
this._collapseButton.icon = ThemeIcon.modify(Codicon.loading, 'spin');
|
||||
} else {
|
||||
const isExpanded = this.expanded.read(r);
|
||||
if (this._collapseButton) {
|
||||
if (this.streamingCompleted || this.element.isComplete) {
|
||||
this._collapseButton.icon = Codicon.check;
|
||||
} else if (!this.fixedScrollingMode) {
|
||||
if (isExpanded) {
|
||||
this._collapseButton.icon = Codicon.chevronDown;
|
||||
} else {
|
||||
this._collapseButton.icon = ThemeIcon.modify(Codicon.loading, 'spin');
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -215,10 +238,6 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
this._onDidChangeHeight.fire();
|
||||
}));
|
||||
|
||||
if (this._collapseButton && !this.streamingCompleted && !this.element.isComplete) {
|
||||
this._collapseButton.icon = ThemeIcon.modify(Codicon.loading, 'spin');
|
||||
}
|
||||
|
||||
const label = this.lastExtractedTitle ?? '';
|
||||
if (!this.fixedScrollingMode && !this._isExpanded.get()) {
|
||||
this.setTitle(label);
|
||||
@@ -264,6 +283,17 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
this.renderMarkdown(this.currentThinkingValue);
|
||||
}
|
||||
|
||||
// Create the persistent working spinner element only if still streaming
|
||||
if (!this.streamingCompleted && !this.element.isComplete) {
|
||||
this.workingSpinnerElement = $('.chat-thinking-item.chat-thinking-spinner-item');
|
||||
const spinnerIcon = createThinkingIcon(ThemeIcon.modify(Codicon.loading, 'spin'));
|
||||
this.workingSpinnerElement.appendChild(spinnerIcon);
|
||||
this.workingSpinnerLabel = $('span.chat-thinking-spinner-label');
|
||||
this.workingSpinnerLabel.textContent = this.getRandomWorkingMessage();
|
||||
this.workingSpinnerElement.appendChild(this.workingSpinnerLabel);
|
||||
this.wrapper.appendChild(this.workingSpinnerElement);
|
||||
}
|
||||
|
||||
// wrap content in scrollable element for fixed scrolling mode
|
||||
if (this.fixedScrollingMode) {
|
||||
this.scrollableElement = this._register(new DomScrollableElement(this.wrapper, {
|
||||
@@ -433,6 +463,17 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
this.setDropdownClickable(!shouldDisable);
|
||||
}
|
||||
|
||||
private appendToWrapper(element: HTMLElement): void {
|
||||
if (!this.wrapper) {
|
||||
return;
|
||||
}
|
||||
if (this.workingSpinnerElement && this.workingSpinnerElement.parentNode === this.wrapper) {
|
||||
this.wrapper.insertBefore(element, this.workingSpinnerElement);
|
||||
} else {
|
||||
this.wrapper.appendChild(element);
|
||||
}
|
||||
}
|
||||
|
||||
public resetId(): void {
|
||||
this.id = undefined;
|
||||
}
|
||||
@@ -497,6 +538,11 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
|
||||
public markAsInactive(): void {
|
||||
this.isActive = false;
|
||||
if (this.workingSpinnerElement) {
|
||||
this.workingSpinnerElement.remove();
|
||||
this.workingSpinnerElement = undefined;
|
||||
this.workingSpinnerLabel = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public finalizeTitleIfDefault(): void {
|
||||
@@ -506,6 +552,12 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
}
|
||||
this.streamingCompleted = true;
|
||||
|
||||
if (this.workingSpinnerElement) {
|
||||
this.workingSpinnerElement.remove();
|
||||
this.workingSpinnerElement = undefined;
|
||||
this.workingSpinnerLabel = undefined;
|
||||
}
|
||||
|
||||
if (this._collapseButton) {
|
||||
this._collapseButton.icon = Codicon.check;
|
||||
}
|
||||
@@ -761,6 +813,11 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
this.trackToolMetadata(toolInvocationId, toolInvocationOrMarkdown);
|
||||
this.appendedItemCount++;
|
||||
|
||||
// get random title
|
||||
if (this.workingSpinnerLabel) {
|
||||
this.workingSpinnerLabel.textContent = this.getRandomWorkingMessage();
|
||||
}
|
||||
|
||||
// If expanded or has been expanded once, render immediately
|
||||
if (this.isExpanded() || this.hasExpandedOnce || (this.fixedScrollingMode && !this.streamingCompleted)) {
|
||||
const result = factory();
|
||||
@@ -967,12 +1024,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
itemWrapper.appendChild(iconElement);
|
||||
itemWrapper.appendChild(content);
|
||||
|
||||
// With lazy rendering, wrapper may not be created yet if content hasn't been expanded
|
||||
if (!this.wrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.wrapper.appendChild(itemWrapper);
|
||||
this.appendToWrapper(itemWrapper);
|
||||
|
||||
if (this.fixedScrollingMode && this.scrollableElement) {
|
||||
setTimeout(() => this.scrollToBottomIfEnabled(), 0);
|
||||
@@ -982,9 +1034,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
private materializeLazyItem(item: ILazyItem): void {
|
||||
if (item.kind === 'thinking') {
|
||||
// Materialize thinking container
|
||||
if (this.wrapper) {
|
||||
this.wrapper.appendChild(item.textContainer);
|
||||
}
|
||||
this.appendToWrapper(item.textContainer);
|
||||
// Store reference to textContainer for updateThinking calls
|
||||
this.textContainer = item.textContainer;
|
||||
this.id = item.content.id;
|
||||
@@ -993,6 +1043,10 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.workingSpinnerLabel) {
|
||||
this.workingSpinnerLabel.textContent = this.getRandomWorkingMessage();
|
||||
}
|
||||
|
||||
// Handle tool items
|
||||
if (item.lazy.hasValue) {
|
||||
return; // Already materialized
|
||||
@@ -1017,9 +1071,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
// Use lazy rendering when collapsed to preserve order with tool items
|
||||
if (this.isExpanded() || this.hasExpandedOnce || (this.fixedScrollingMode && !this.streamingCompleted)) {
|
||||
// Render immediately when expanded
|
||||
if (this.wrapper) {
|
||||
this.wrapper.appendChild(this.textContainer);
|
||||
}
|
||||
this.appendToWrapper(this.textContainer);
|
||||
this.id = content.id;
|
||||
this.updateThinking(content);
|
||||
} else {
|
||||
@@ -1035,6 +1087,10 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
};
|
||||
this.lazyItems.push(lazyThinking);
|
||||
}
|
||||
|
||||
if (this.workingSpinnerLabel) {
|
||||
this.workingSpinnerLabel.textContent = this.getRandomWorkingMessage();
|
||||
}
|
||||
}
|
||||
this.updateDropdownClickability();
|
||||
}
|
||||
@@ -1072,6 +1128,11 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
||||
this.markdownResult.dispose();
|
||||
this.markdownResult = undefined;
|
||||
}
|
||||
if (this.workingSpinnerElement) {
|
||||
this.workingSpinnerElement.remove();
|
||||
this.workingSpinnerElement = undefined;
|
||||
this.workingSpinnerLabel = undefined;
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,23 @@
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
/* shimmer animation stuffs */
|
||||
.chat-thinking-spinner-item .chat-thinking-spinner-label {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--vscode-descriptionForeground) 0%,
|
||||
var(--vscode-descriptionForeground) 30%,
|
||||
var(--vscode-chat-thinkingShimmer) 50%,
|
||||
var(--vscode-descriptionForeground) 70%,
|
||||
var(--vscode-descriptionForeground) 100%
|
||||
);
|
||||
background-size: 400% 100%;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: chat-thinking-shimmer 2s linear infinite;
|
||||
}
|
||||
|
||||
&:not(.chat-used-context-collapsed) .chat-used-context-list.chat-thinking-collapsible {
|
||||
overflow: visible;
|
||||
max-height: none;
|
||||
@@ -56,8 +73,8 @@
|
||||
overflow: hidden;
|
||||
padding: 10px 10px 10px 2px;
|
||||
|
||||
.codicon:not(.codicon-check) {
|
||||
display: inline-flex;
|
||||
.codicon:not(.codicon-check, .codicon-loading) {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +141,8 @@
|
||||
|
||||
/* chain of thought lines */
|
||||
.chat-thinking-tool-wrapper,
|
||||
.chat-thinking-item.markdown-content {
|
||||
.chat-thinking-item.markdown-content,
|
||||
.chat-thinking-spinner-item {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
@@ -170,6 +188,15 @@
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.chat-thinking-spinner-item {
|
||||
padding: 6px 12px 6px 24px;
|
||||
font-size: var(--vscode-chat-font-size-body-s);
|
||||
|
||||
.chat-thinking-spinner-label {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-thinking-item {
|
||||
|
||||
@@ -861,6 +861,11 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
|
||||
// Show if no content, only "used references", ends with a complete tool call, or ends with complete text edits and there is no incomplete tool call (edits are still being applied some time after they are all generated)
|
||||
const lastPart = findLast(partsToRender, part => part.kind !== 'markdownContent' || part.content.value.trim().length > 0);
|
||||
|
||||
// don't show working progress when there is thinking content in partsToRender (about to be rendered)
|
||||
if (partsToRender.some(part => part.kind === 'thinking')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// never show working progress when there is an active thinking piece
|
||||
const lastThinking = this.getLastThinkingPart(templateData.renderedParts);
|
||||
if (lastThinking) {
|
||||
|
||||
@@ -120,6 +120,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes chat-thinking-shimmer {
|
||||
0% {
|
||||
background-position: 120% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -120% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.interactive-item-container .chat-animated-ellipsis::after {
|
||||
content: '';
|
||||
white-space: nowrap;
|
||||
@@ -2247,6 +2256,23 @@ have to be updated for changes to the rules above, or to support more deeply nes
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
/* shimmer animation stuffs */
|
||||
&:has(.codicon-loading) .rendered-markdown.progress-step > p {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--vscode-descriptionForeground) 0%,
|
||||
var(--vscode-descriptionForeground) 30%,
|
||||
var(--vscode-chat-thinkingShimmer) 50%,
|
||||
var(--vscode-descriptionForeground) 70%,
|
||||
var(--vscode-descriptionForeground) 100%
|
||||
);
|
||||
background-size: 400% 100%;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: chat-thinking-shimmer 2s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.interactive-item-container .chat-command-button {
|
||||
|
||||
@@ -73,3 +73,8 @@ export const chatLinesRemovedForeground = registerColor(
|
||||
'chat.linesRemovedForeground',
|
||||
{ dark: '#FC6A6A', light: '#BC2F32', hcDark: '#F48771', hcLight: '#B5200D' },
|
||||
localize('chat.linesRemovedForeground', 'Foreground color of lines removed in chat code block pill.'), true);
|
||||
|
||||
export const chatThinkingShimmer = registerColor(
|
||||
'chat.thinkingShimmer',
|
||||
{ dark: '#ffffff', light: '#000000', hcDark: '#ffffff', hcLight: '#000000' },
|
||||
localize('chat.thinkingShimmer', 'Shimmer highlight for thinking/working labels.'), true);
|
||||
|
||||
Reference in New Issue
Block a user