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
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
import { useMarkdownRendering } from '../chat-response-renderer/markdown-part-renderer';
import { ProgressMessage } from '../chat-progress-message';
import { AIChatTreeInputFactory, type AIChatTreeInputWidget } from './chat-view-tree-input-widget';
import { PromptVariantBadge } from './prompt-variant-badge';

// TODO Instead of directly operating on the ChatRequestModel we could use an intermediate view model
export interface RequestNode extends TreeNode {
Expand Down Expand Up @@ -522,6 +523,10 @@ export class ChatViewTreeWidget extends TreeWidget {
: [];
const agentLabel = React.createRef<HTMLHeadingElement>();
const agentDescription = this.getAgent(node)?.description;

const promptVariantId = isResponseNode(node) ? node.response.promptVariantId : undefined;
const isPromptVariantEdited = isResponseNode(node) ? !!node.response.isPromptVariantEdited : false;

return <React.Fragment>
<div className='theia-ChatNodeHeader'>
<div className={`theia-AgentAvatar ${this.getAgentIconClassName(node)}`}></div>
Expand All @@ -538,6 +543,13 @@ export class ChatViewTreeWidget extends TreeWidget {
}}>
{this.getAgentLabel(node)}
</h3>
{promptVariantId && (
<PromptVariantBadge
variantId={promptVariantId}
isEdited={isPromptVariantEdited}
hoverService={this.hoverService}
/>
)}
{inProgress && !waitingForInput &&
<span className='theia-ChatContentInProgress'>
{nls.localize('theia/ai/chat-ui/chat-view-tree-widget/generating', 'Generating')}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import * as React from '@theia/core/shared/react';
import { nls } from '@theia/core';
import { HoverService } from '@theia/core/lib/browser';

export interface PromptVariantBadgeProps {
variantId: string;
isEdited: boolean;
hoverService: HoverService;
}

export const PromptVariantBadge: React.FC<PromptVariantBadgeProps> = ({ variantId, isEdited, hoverService }) => {
// eslint-disable-next-line no-null/no-null
const badgeRef = React.useRef<HTMLSpanElement>(null);
const displayText = isEdited
? `[${nls.localize('theia/ai/chat-ui/edited', 'edited')}] ${variantId}`
: variantId;
const baseTooltip = nls.localize('theia/ai/chat-ui/variantTooltip', 'Prompt variant: {0}', variantId);
const tooltip = isEdited
? baseTooltip + '. ' + nls.localize('theia/ai/chat-ui/editedTooltipHint', 'This prompt variant has been edited. You can reset it in the AI Configuration view.')
: baseTooltip;

return (
<span
ref={badgeRef}
className={`theia-PromptVariantBadge ${isEdited ? 'edited' : ''}`}
onMouseEnter={() => {
if (badgeRef.current) {
hoverService.requestHover({
content: tooltip,
target: badgeRef.current,
position: 'right'
});
};
}}
>
{displayText}
</span>
);
};
16 changes: 16 additions & 0 deletions packages/ai-chat-ui/src/browser/style/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,22 @@ div:last-child > .theia-ChatNode {
font-weight: 600;
}

.theia-PromptVariantBadge {
padding: 0 calc(var(--theia-ui-padding) * 2 / 3);
font-size: var(--theia-ui-font-size0);
color: var(--theia-badge-foreground);
background-color: var(--theia-badge-background);
border-radius: calc(var(--theia-ui-padding) * 2 / 3);
white-space: nowrap;
user-select: none;
}

.theia-PromptVariantBadge.edited {
color: var(--theia-editorWarning-foreground);
background-color: var(--theia-inputValidation-warningBackground);
border: 1px solid var(--theia-editorWarning-foreground);
}

.theia-ChatNode .theia-ChatNodeToolbar {
margin-left: auto;
line-height: 18px;
Expand Down
34 changes: 31 additions & 3 deletions packages/ai-chat/src/common/chat-agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
AIVariableContext,
AIVariableResolutionRequest,
getTextOfResponse,
isCustomizedPromptFragment,
isLanguageModelStreamResponsePart,
isTextResponsePart,
isThinkingResponsePart,
Expand Down Expand Up @@ -76,12 +77,22 @@ export interface SystemMessageDescription {
text: string;
/** All functions references in the system message. */
functionDescriptions?: Map<string, ToolRequest>;
/** The prompt variant ID used */
promptVariantId?: string;
/** Whether the prompt variant is customized */
isPromptVariantEdited?: boolean;
}
export namespace SystemMessageDescription {
export function fromResolvedPromptFragment(resolvedPrompt: ResolvedPromptFragment): SystemMessageDescription {
export function fromResolvedPromptFragment(
resolvedPrompt: ResolvedPromptFragment,
promptVariantId?: string,
isPromptVariantEdited?: boolean
): SystemMessageDescription {
return {
text: resolvedPrompt.text,
functionDescriptions: resolvedPrompt.functionDescriptions
functionDescriptions: resolvedPrompt.functionDescriptions,
promptVariantId,
isPromptVariantEdited
};
}
}
Expand Down Expand Up @@ -192,6 +203,14 @@ export abstract class AbstractChatAgent implements ChatAgent {
throw new Error(nls.localize('theia/ai/chat/couldNotFindMatchingLM', 'Couldn\'t find a matching language model. Please check your setup!'));
}
const systemMessageDescription = await this.getSystemMessageDescription({ model: request.session, request } satisfies ChatSessionContext);

if (systemMessageDescription?.promptVariantId) {
request.response.setPromptVariantInfo(
systemMessageDescription.promptVariantId,
systemMessageDescription.isPromptVariantEdited ?? false
);
}

const messages = await this.getMessages(request.session);

if (systemMessageDescription) {
Expand Down Expand Up @@ -255,8 +274,17 @@ export abstract class AbstractChatAgent implements ChatAgent {
if (this.systemPromptId === undefined) {
return undefined;
}

const effectiveVariantId = this.promptService.getEffectiveVariantId(this.systemPromptId) ?? this.systemPromptId;
const isEdited = this.isPromptVariantCustomized(effectiveVariantId);

const resolvedPrompt = await this.promptService.getResolvedPromptFragment(this.systemPromptId, undefined, context);
return resolvedPrompt ? SystemMessageDescription.fromResolvedPromptFragment(resolvedPrompt) : undefined;
return resolvedPrompt ? SystemMessageDescription.fromResolvedPromptFragment(resolvedPrompt, effectiveVariantId, isEdited) : undefined;
}

protected isPromptVariantCustomized(fragmentId: string): boolean {
const fragment = this.promptService.getRawPromptFragment(fragmentId);
return fragment ? isCustomizedPromptFragment(fragment) : false;
}

protected async getMessages(
Expand Down
2 changes: 2 additions & 0 deletions packages/ai-chat/src/common/chat-model-serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export interface SerializableChatResponseData {
isComplete: boolean;
isError: boolean;
errorMessage?: string;
promptVariantId?: string;
isPromptVariantEdited?: boolean;
content: SerializableChatResponseContentData[];
}

Expand Down
28 changes: 28 additions & 0 deletions packages/ai-chat/src/common/chat-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,14 @@ export interface ChatResponseModel {
* This can be used to store and retrieve such data.
*/
readonly data: { [key: string]: unknown };
/**
* The ID of the prompt variant used to generate this response
*/
readonly promptVariantId?: string;
/**
* Indicates whether the prompt variant was customized/edited
*/
readonly isPromptVariantEdited?: boolean;
toSerializable(): SerializableChatResponseData;
}

Expand Down Expand Up @@ -2331,6 +2339,8 @@ export class MutableChatResponseModel implements ChatResponseModel {
protected _isError: boolean;
protected _errorObject: Error | undefined;
protected _cancellationToken: CancellationTokenSource;
protected _promptVariantId?: string;
protected _isPromptVariantEdited?: boolean;

constructor(
requestId: string,
Expand Down Expand Up @@ -2372,6 +2382,8 @@ export class MutableChatResponseModel implements ChatResponseModel {
this._isWaitingForInput = false;
// TODO: Restore progressMessages?
this._progressMessages = [];
this._promptVariantId = data.promptVariantId;
this._isPromptVariantEdited = data.isPromptVariantEdited ?? false;

if (data.errorMessage) {
this._errorObject = new Error(data.errorMessage);
Expand Down Expand Up @@ -2443,6 +2455,20 @@ export class MutableChatResponseModel implements ChatResponseModel {
return this._agentId;
}

get promptVariantId(): string | undefined {
return this._promptVariantId;
}

get isPromptVariantEdited(): boolean {
return this._isPromptVariantEdited ?? false;
}

setPromptVariantInfo(variantId: string | undefined, isEdited: boolean): void {
this._promptVariantId = variantId;
this._isPromptVariantEdited = isEdited;
this._onDidChangeEmitter.fire();
}

overrideAgentId(agentId: string): void {
this._agentId = agentId;
}
Expand Down Expand Up @@ -2508,6 +2534,8 @@ export class MutableChatResponseModel implements ChatResponseModel {
isComplete: this.isComplete,
isError: this.isError,
errorMessage: this.errorObject?.message,
promptVariantId: this._promptVariantId,
isPromptVariantEdited: this._isPromptVariantEdited,
content: this.response.content.map(c => {
const serialized = c.toSerializable?.();
if (!serialized) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { PromptService, PromptVariantSet } from '@theia/ai-core/lib/common';
import { isCustomizedPromptFragment, PromptService, PromptVariantSet } from '@theia/ai-core/lib/common';
import * as React from '@theia/core/shared/react';
import { nls } from '@theia/core/lib/common/nls';

Expand All @@ -32,6 +32,11 @@ export const PromptVariantRenderer: React.FC<PromptVariantRendererProps> = ({
const defaultVariantId = promptService.getDefaultVariantId(promptVariantSet.id);
const [selectedVariant, setSelectedVariant] = React.useState<string>(defaultVariantId!);

const isVariantCustomized = (variantId: string): boolean => {
const fragment = promptService.getRawPromptFragment(variantId);
return fragment ? isCustomizedPromptFragment(fragment) : false;
};

React.useEffect(() => {
const currentVariant = promptService.getSelectedVariantId(promptVariantSet.id);
setSelectedVariant(currentVariant ?? defaultVariantId!);
Expand Down Expand Up @@ -79,14 +84,25 @@ export const PromptVariantRenderer: React.FC<PromptVariantRendererProps> = ({
{nls.localize('theia/ai/core/templateSettings/unavailableVariant', 'Unavailable')}
</option>
)}
{variantIds.map(variantId => (
<option key={variantId} value={variantId}>
{variantId === defaultVariantId ? variantId + ' ' + nls.localizeByDefault('(default)') : variantId}
</option>
))}
{variantIds.map(variantId => {
const isEdited = isVariantCustomized(variantId);
const editedPrefix = isEdited ? `[${nls.localize('theia/ai/core/templateSettings/edited', 'edited')}] ` : '';
const defaultSuffix = variantId === defaultVariantId ? ' ' + nls.localizeByDefault('(default)') : '';
return (
<option key={variantId} value={variantId}>
{editedPrefix}{variantId}{defaultSuffix}
</option>
);
})}
</select>
)}
{variantIds.length === 1 && !isInvalidVariant && <span>{selectedVariant}</span>}
{variantIds.length === 1 && !isInvalidVariant && (
<span>
{isVariantCustomized(selectedVariant)
? `[${nls.localize('theia/ai/core/templateSettings/edited', 'edited')}] ${selectedVariant}`
: selectedVariant}
</span>
)}
</td>
<td className="template-actions-cell">
<button
Expand All @@ -95,12 +111,13 @@ export const PromptVariantRenderer: React.FC<PromptVariantRendererProps> = ({
disabled={isInvalidVariant}
title={nls.localizeByDefault('Edit')}
/>
<button
className="template-action-icon-button codicon codicon-discard"
onClick={resetTemplate}
disabled={isInvalidVariant}
title={nls.localizeByDefault('Reset')}
/>
{isVariantCustomized(selectedVariant) &&
(<button
className="template-action-icon-button codicon codicon-discard"
onClick={resetTemplate}
disabled={isInvalidVariant || !isVariantCustomized(selectedVariant)}
title={nls.localizeByDefault('Reset')}
/>)}
</td>
</tr>
</>
Expand Down
Loading