Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/vs/workbench/api/common/extHostLanguageModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape {
tooltip: m.tooltip,
version: m.version,
multiplier: m.multiplier,
multiplierNumeric: m.multiplierNumeric,
maxInputTokens: m.maxInputTokens,
maxOutputTokens: m.maxOutputTokens,
auth,
Expand Down
12 changes: 12 additions & 0 deletions src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatService } from '.
import { isResponseVM } from '../../common/model/chatViewModel.js';
import { ChatModeKind } from '../../common/constants.js';
import { IChatAccessibilityService, IChatWidgetService } from '../chat.js';
import { triggerConfetti } from '../widget/chatConfetti.js';
import { CHAT_CATEGORY } from './chatActions.js';
import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js';

export const MarkUnhelpfulActionId = 'workbench.action.chat.markUnhelpful';
const enableFeedbackConfig = 'config.telemetry.feedback.enabled';
Expand Down Expand Up @@ -75,6 +77,16 @@ export function registerChatTitleActions() {
});
item.setVote(ChatAgentVoteDirection.Up);
item.setVoteDownReason(undefined);

const configurationService = accessor.get(IConfigurationService);
const accessibilityService = accessor.get(IAccessibilityService);
if (configurationService.getValue<boolean>('chat.confettiOnThumbsUp') && !accessibilityService.isMotionReduced()) {
const chatWidgetService = accessor.get(IChatWidgetService);
const widget = chatWidgetService.getWidgetBySessionResource(item.session.sessionResource);
if (widget) {
triggerConfetti(widget.domNode);
}
}
}
});

Expand Down
5 changes: 5 additions & 0 deletions src/vs/workbench/contrib/chat/browser/chat.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,11 @@ configurationRegistry.registerConfiguration({
mode: 'auto'
}
},
'chat.confettiOnThumbsUp': {
type: 'boolean',
description: nls.localize('chat.confettiOnThumbsUp', "Controls whether a confetti animation is shown when clicking the thumbs up button on a chat response."),
default: false,
},
'chat.experimental.detectParticipant.enabled': {
type: 'boolean',
deprecationMessage: nls.localize('chat.experimental.detectParticipant.enabled.deprecated', "This setting is deprecated. Please use `chat.detectParticipant.enabled` instead."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -821,10 +821,12 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo
if (tool.impl!.prepareToolInvocation) {
const preparePromise = tool.impl!.prepareToolInvocation({
parameters: dto.parameters,
toolCallId: dto.callId,
chatRequestId: dto.chatRequestId,
chatSessionId: dto.context?.sessionId,
chatSessionResource: dto.context?.sessionResource,
chatInteractionId: dto.chatInteractionId,
modelId: dto.modelId,
forceConfirmationReason: forceConfirmationReason
}, token);

Expand Down
85 changes: 85 additions & 0 deletions src/vs/workbench/contrib/chat/browser/widget/chatConfetti.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as dom from '../../../../../base/browser/dom.js';

const confettiColors = [
'#f44336', '#e91e63', '#9c27b0', '#673ab7',
'#3f51b5', '#2196f3', '#03a9f4', '#00bcd4',
'#009688', '#4caf50', '#8bc34a', '#ffeb3b',
'#ffc107', '#ff9800', '#ff5722'
];

let activeOverlay: HTMLElement | undefined;

/**
* Triggers a confetti animation inside the given container element.
*/
export function triggerConfetti(container: HTMLElement) {
if (activeOverlay) {
return;
}

const overlay = dom.$('.chat-confetti-overlay');
overlay.style.position = 'absolute';
overlay.style.inset = '0';
overlay.style.pointerEvents = 'none';
overlay.style.overflow = 'hidden';
overlay.style.zIndex = '1000';
container.appendChild(overlay);
activeOverlay = overlay;

const { width, height } = container.getBoundingClientRect();
for (let i = 0; i < 250; i++) {
const part = dom.$('.chat-confetti-particle');
part.style.position = 'absolute';
part.style.width = `${Math.random() * 8 + 4}px`;
part.style.height = `${Math.random() * 8 + 4}px`;
part.style.backgroundColor = confettiColors[Math.floor(Math.random() * confettiColors.length)];
part.style.borderRadius = Math.random() > 0.5 ? '50%' : '0';
part.style.left = `${Math.random() * width}px`;
part.style.top = '-10px';
part.style.opacity = '1';

overlay.appendChild(part);

const targetX = (Math.random() - 0.5) * width * 0.8;
const targetY = Math.random() * height * 0.8 + height * 0.1;
const rotation = Math.random() * 720 - 360;
const duration = Math.random() * 1000 + 1500;
const delay = Math.random() * 400;

part.animate([
{
transform: 'translate(0, 0) rotate(0deg)',
opacity: 1
},
{
transform: `translate(${targetX * 0.5}px, ${targetY * 0.5}px) rotate(${rotation * 0.5}deg)`,
opacity: 1,
offset: 0.3
},
{
transform: `translate(${targetX}px, ${targetY}px) rotate(${rotation}deg)`,
opacity: 1,
offset: 0.75
},
{
transform: `translate(${targetX * 1.1}px, ${targetY + 40}px) rotate(${rotation + 30}deg)`,
opacity: 0
}
], {
duration,
delay,
easing: 'linear',
fill: 'forwards'
});
}

setTimeout(() => {
overlay.remove();
activeOverlay = undefined;
}, 3000);
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export abstract class ChatCollapsibleContentPart extends Disposable implements I
private title: IMarkdownString | string,
context: IChatContentPartRenderContext,
private readonly hoverMessage: IMarkdownString | undefined,
@IHoverService private readonly hoverService: IHoverService,
@IHoverService protected readonly hoverService: IHoverService,
) {
super();
this.element = context.element;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import * as dom from '../../../../../../base/browser/dom.js';
import { $, AnimationFrameScheduler, DisposableResizeObserver } from '../../../../../../base/browser/dom.js';
import { Codicon } from '../../../../../../base/common/codicons.js';
import { IDisposable } from '../../../../../../base/common/lifecycle.js';
import { IDisposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js';
import { ThemeIcon } from '../../../../../../base/common/themables.js';
import { rcut } from '../../../../../../base/common/strings.js';
import { localize } from '../../../../../../nls.js';
Expand Down Expand Up @@ -79,6 +79,10 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
// Current tool message for collapsed title (persists even after tool completes)
private currentRunningToolMessage: string | undefined;

// Model name used by this subagent for hover tooltip
private modelName: string | undefined;
private readonly _hoverDisposable = this._register(new MutableDisposable());

// Confirmation auto-expand tracking
private toolsWaitingForConfirmation: number = 0;
private userManuallyExpanded: boolean = false;
Expand All @@ -87,11 +91,11 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
/**
* Extracts subagent info (description, agentName, prompt) from a tool invocation.
*/
private static extractSubagentInfo(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): { description: string; agentName: string | undefined; prompt: string | undefined } {
private static extractSubagentInfo(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): { description: string; agentName: string | undefined; prompt: string | undefined; modelName: string | undefined } {
const defaultDescription = localize('chat.subagent.defaultDescription', 'Running subagent...');

if (toolInvocation.toolId !== RunSubagentTool.Id) {
return { description: defaultDescription, agentName: undefined, prompt: undefined };
return { description: defaultDescription, agentName: undefined, prompt: undefined, modelName: undefined };
}

// Check toolSpecificData first (works for both live and serialized)
Expand All @@ -100,6 +104,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
description: toolInvocation.toolSpecificData.description ?? defaultDescription,
agentName: toolInvocation.toolSpecificData.agentName,
prompt: toolInvocation.toolSpecificData.prompt,
modelName: toolInvocation.toolSpecificData.modelName,
};
}

Expand All @@ -113,10 +118,11 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
description: params?.description ?? defaultDescription,
agentName: params?.agentName,
prompt: params?.prompt,
modelName: undefined,
};
}

return { description: defaultDescription, agentName: undefined, prompt: undefined };
return { description: defaultDescription, agentName: undefined, prompt: undefined, modelName: undefined };
}

constructor(
Expand All @@ -134,7 +140,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
@IHoverService hoverService: IHoverService,
) {
// Extract description, agentName, and prompt from toolInvocation
const { description, agentName, prompt } = ChatSubagentContentPart.extractSubagentInfo(toolInvocation);
const { description, agentName, prompt, modelName } = ChatSubagentContentPart.extractSubagentInfo(toolInvocation);

// Build title: "AgentName: description" or "Subagent: description"
const prefix = agentName || localize('chat.subagent.prefix', 'Subagent');
Expand All @@ -144,6 +150,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
this.description = description;
this.agentName = agentName;
this.prompt = prompt;
this.modelName = modelName;
this.isInitiallyComplete = this.element.isComplete;

const node = this.domNode;
Expand Down Expand Up @@ -201,6 +208,9 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
// Scheduler for coalescing layout operations
this.layoutScheduler = this._register(new AnimationFrameScheduler(this.domNode, () => this.performLayout()));

// Set up hover tooltip with model name if available
this.updateHover();

// Render the prompt section at the start if available (must be after wrapper is initialized)
this.renderPromptSection();

Expand Down Expand Up @@ -327,6 +337,16 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
this.setTitleWithWidgets(new MarkdownString(finalLabel), this.instantiationService, this.chatMarkdownAnchorService, this.chatContentMarkdownRenderer);
}

private updateHover(): void {
if (!this.modelName || !this._collapseButton) {
return;
}

this._hoverDisposable.value = this.hoverService.setupDelayedHover(this._collapseButton.element, {
content: localize('chat.subagent.modelTooltip', 'Model: {0}', this.modelName),
});
}

/**
* Tracks a tool invocation's state for:
* 1. Updating the title with the current tool message (persists even after completion)
Expand Down Expand Up @@ -400,15 +420,25 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
this.renderResultText(textParts.join('\n'));
}

// Update model name from toolSpecificData (set during invoke())
if (toolInvocation.toolSpecificData?.kind === 'subagent' && toolInvocation.toolSpecificData.modelName) {
this.modelName = toolInvocation.toolSpecificData.modelName;
this.updateHover();
}

// Mark as inactive when the tool completes
this.markAsInactive();
} else if (wasStreaming && state.type !== IChatToolInvocation.StateKind.Streaming) {
wasStreaming = false;
// Update things that change when tool is done streaming
const { description, agentName, prompt } = ChatSubagentContentPart.extractSubagentInfo(toolInvocation);
const { description, agentName, prompt, modelName } = ChatSubagentContentPart.extractSubagentInfo(toolInvocation);
this.description = description;
this.agentName = agentName;
this.prompt = prompt;
if (modelName) {
this.modelName = modelName;
this.updateHover();
}
this.renderPromptSection();
this.updateTitle();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,7 @@ export interface IChatSubagentToolInvocationData {
agentName?: string;
prompt?: string;
result?: string;
modelName?: string;
}

export interface IChatTodoListContent {
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/contrib/chat/common/languageModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ export interface ILanguageModelChatMetadata {
readonly tooltip?: string;
readonly detail?: string;
readonly multiplier?: string;
readonly multiplierNumeric?: number;
readonly family: string;
readonly maxInputTokens: number;
readonly maxOutputTokens: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const CLAUDE_HOOK_TYPE_MAP: Record<string, HookType> = {
'UserPromptSubmit': HookType.UserPromptSubmit,
'PreToolUse': HookType.PreToolUse,
'PostToolUse': HookType.PostToolUse,
'PreCompact': HookType.PreCompact,
'SubagentStart': HookType.SubagentStart,
'SubagentStop': HookType.SubagentStop,
'Stop': HookType.Stop,
Expand Down
11 changes: 11 additions & 0 deletions src/vs/workbench/contrib/chat/common/promptSyntax/hookSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export enum HookType {
UserPromptSubmit = 'UserPromptSubmit',
PreToolUse = 'PreToolUse',
PostToolUse = 'PostToolUse',
PreCompact = 'PreCompact',
SubagentStart = 'SubagentStart',
SubagentStop = 'SubagentStop',
Stop = 'Stop',
Expand Down Expand Up @@ -53,6 +54,11 @@ export const HOOK_TYPES = [
label: nls.localize('hookType.postToolUse.label', "Post-Tool Use"),
description: nls.localize('hookType.postToolUse.description', "Executed after a tool completes execution successfully.")
},
{
id: HookType.PreCompact,
label: nls.localize('hookType.preCompact.label', "Pre-Compact"),
description: nls.localize('hookType.preCompact.description', "Executed before the agent compacts the conversation context.")
},
{
id: HookType.SubagentStart,
label: nls.localize('hookType.subagentStart.label', "Subagent Start"),
Expand Down Expand Up @@ -104,6 +110,7 @@ export interface IChatRequestHooks {
readonly [HookType.UserPromptSubmit]?: readonly IHookCommand[];
readonly [HookType.PreToolUse]?: readonly IHookCommand[];
readonly [HookType.PostToolUse]?: readonly IHookCommand[];
readonly [HookType.PreCompact]?: readonly IHookCommand[];
readonly [HookType.SubagentStart]?: readonly IHookCommand[];
readonly [HookType.SubagentStop]?: readonly IHookCommand[];
readonly [HookType.Stop]?: readonly IHookCommand[];
Expand Down Expand Up @@ -198,6 +205,10 @@ export const hookFileSchema: IJSONSchema = {
...hookArraySchema,
description: nls.localize('hookFile.postToolUse', 'Executed after a tool completes execution successfully. Use to log execution results, track usage statistics, generate audit trails, or monitor performance.')
},
PreCompact: {
...hookArraySchema,
description: nls.localize('hookFile.preCompact', 'Executed before the agent compacts the conversation context. Use to save conversation state, export important information, or prepare for context reduction.')
},
SubagentStart: {
...hookArraySchema,
description: nls.localize('hookFile.subagentStart', 'Executed when a subagent is started. Use to log subagent spawning, track nested agent usage, or initialize subagent-specific resources.')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,7 @@ export class PromptsService extends Disposable implements IPromptsService {
[HookType.UserPromptSubmit]: [],
[HookType.PreToolUse]: [],
[HookType.PostToolUse]: [],
[HookType.PreCompact]: [],
[HookType.SubagentStart]: [],
[HookType.SubagentStop]: [],
[HookType.Stop]: [],
Expand Down
Loading
Loading