Skip to content

Commit eeca2e3

Browse files
committed
feat: display prompt variant ID in chat view responses
- Extend the `ChatResponseModel` with two optional properties `promptVariantId` and `isPromptVariantEdited` to faciliate displaying these - Persist the properties as part of the `SerializableChatResponseData` - Show the prompt variant ID next to the agent name for AI responses. - Customized variants are prefixed with `[edited]` and include a tooltip explaining how to reset them in the AI Configuration view.
1 parent 99f7b12 commit eeca2e3

File tree

6 files changed

+143
-3
lines changed

6 files changed

+143
-3
lines changed

packages/ai-chat-ui/src/browser/chat-tree-view/chat-view-tree-widget.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
6060
import { useMarkdownRendering } from '../chat-response-renderer/markdown-part-renderer';
6161
import { ProgressMessage } from '../chat-progress-message';
6262
import { AIChatTreeInputFactory, type AIChatTreeInputWidget } from './chat-view-tree-input-widget';
63+
import { PromptVariantBadge } from './prompt-variant-badge';
6364

6465
// TODO Instead of directly operating on the ChatRequestModel we could use an intermediate view model
6566
export interface RequestNode extends TreeNode {
@@ -522,6 +523,10 @@ export class ChatViewTreeWidget extends TreeWidget {
522523
: [];
523524
const agentLabel = React.createRef<HTMLHeadingElement>();
524525
const agentDescription = this.getAgent(node)?.description;
526+
527+
const promptVariantId = isResponseNode(node) ? node.response.promptVariantId : undefined;
528+
const isPromptVariantEdited = isResponseNode(node) ? !!node.response.isPromptVariantEdited : false;
529+
525530
return <React.Fragment>
526531
<div className='theia-ChatNodeHeader'>
527532
<div className={`theia-AgentAvatar ${this.getAgentIconClassName(node)}`}></div>
@@ -538,6 +543,13 @@ export class ChatViewTreeWidget extends TreeWidget {
538543
}}>
539544
{this.getAgentLabel(node)}
540545
</h3>
546+
{promptVariantId && (
547+
<PromptVariantBadge
548+
variantId={promptVariantId}
549+
isEdited={isPromptVariantEdited}
550+
hoverService={this.hoverService}
551+
/>
552+
)}
541553
{inProgress && !waitingForInput &&
542554
<span className='theia-ChatContentInProgress'>
543555
{nls.localize('theia/ai/chat-ui/chat-view-tree-widget/generating', 'Generating')}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2025 EclipseSource GmbH.
3+
//
4+
// This program and the accompanying materials are made available under the
5+
// terms of the Eclipse Public License v. 2.0 which is available at
6+
// http://www.eclipse.org/legal/epl-2.0.
7+
//
8+
// This Source Code may also be made available under the following Secondary
9+
// Licenses when the conditions for such availability set forth in the Eclipse
10+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
// with the GNU Classpath Exception which is available at
12+
// https://www.gnu.org/software/classpath/license.html.
13+
//
14+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15+
// *****************************************************************************
16+
import * as React from '@theia/core/shared/react';
17+
import { nls } from '@theia/core';
18+
import { HoverService } from '@theia/core/lib/browser';
19+
20+
export interface PromptVariantBadgeProps {
21+
variantId: string;
22+
isEdited: boolean;
23+
hoverService: HoverService;
24+
}
25+
26+
export const PromptVariantBadge: React.FC<PromptVariantBadgeProps> = ({ variantId, isEdited, hoverService }) => {
27+
// eslint-disable-next-line no-null/no-null
28+
const badgeRef = React.useRef<HTMLSpanElement>(null);
29+
const displayText = isEdited
30+
? `[${nls.localize('theia/ai/chat-ui/edited', 'edited')}] ${variantId}`
31+
: variantId;
32+
const baseTooltip = nls.localize('theia/ai/chat-ui/variantTooltip', 'Prompt variant: {0}', variantId);
33+
const tooltip = isEdited
34+
? baseTooltip + '. ' + nls.localize('theia/ai/chat-ui/editedTooltipHint', 'This prompt variant has been edited. You can reset it in the AI Configuration view.')
35+
: baseTooltip;
36+
37+
return (
38+
<span
39+
ref={badgeRef}
40+
className={`theia-PromptVariantBadge ${isEdited ? 'edited' : ''}`}
41+
onMouseEnter={() => {
42+
if (badgeRef.current) {
43+
hoverService.requestHover({
44+
content: tooltip,
45+
target: badgeRef.current,
46+
position: 'right'
47+
});
48+
};
49+
}}
50+
>
51+
{displayText}
52+
</span>
53+
);
54+
};

packages/ai-chat-ui/src/browser/style/index.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,22 @@ div:last-child > .theia-ChatNode {
9191
font-weight: 600;
9292
}
9393

94+
.theia-PromptVariantBadge {
95+
padding: 0 calc(var(--theia-ui-padding) * 2 / 3);
96+
font-size: var(--theia-ui-font-size0);
97+
color: var(--theia-badge-foreground);
98+
background-color: var(--theia-badge-background);
99+
border-radius: calc(var(--theia-ui-padding) * 2 / 3);
100+
white-space: nowrap;
101+
user-select: none;
102+
}
103+
104+
.theia-PromptVariantBadge.edited {
105+
color: var(--theia-editorWarning-foreground);
106+
background-color: var(--theia-inputValidation-warningBackground);
107+
border: 1px solid var(--theia-editorWarning-foreground);
108+
}
109+
94110
.theia-ChatNode .theia-ChatNodeToolbar {
95111
margin-left: auto;
96112
line-height: 18px;

packages/ai-chat/src/common/chat-agents.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
AIVariableContext,
2525
AIVariableResolutionRequest,
2626
getTextOfResponse,
27+
isCustomizedPromptFragment,
2728
isLanguageModelStreamResponsePart,
2829
isTextResponsePart,
2930
isThinkingResponsePart,
@@ -76,12 +77,22 @@ export interface SystemMessageDescription {
7677
text: string;
7778
/** All functions references in the system message. */
7879
functionDescriptions?: Map<string, ToolRequest>;
80+
/** The prompt variant ID used */
81+
promptVariantId?: string;
82+
/** Whether the prompt variant is customized */
83+
isPromptVariantEdited?: boolean;
7984
}
8085
export namespace SystemMessageDescription {
81-
export function fromResolvedPromptFragment(resolvedPrompt: ResolvedPromptFragment): SystemMessageDescription {
86+
export function fromResolvedPromptFragment(
87+
resolvedPrompt: ResolvedPromptFragment,
88+
promptVariantId?: string,
89+
isPromptVariantEdited?: boolean
90+
): SystemMessageDescription {
8291
return {
8392
text: resolvedPrompt.text,
84-
functionDescriptions: resolvedPrompt.functionDescriptions
93+
functionDescriptions: resolvedPrompt.functionDescriptions,
94+
promptVariantId,
95+
isPromptVariantEdited
8596
};
8697
}
8798
}
@@ -192,6 +203,14 @@ export abstract class AbstractChatAgent implements ChatAgent {
192203
throw new Error(nls.localize('theia/ai/chat/couldNotFindMatchingLM', 'Couldn\'t find a matching language model. Please check your setup!'));
193204
}
194205
const systemMessageDescription = await this.getSystemMessageDescription({ model: request.session, request } satisfies ChatSessionContext);
206+
207+
if (systemMessageDescription?.promptVariantId) {
208+
request.response.setPromptVariantInfo(
209+
systemMessageDescription.promptVariantId,
210+
systemMessageDescription.isPromptVariantEdited ?? false
211+
);
212+
}
213+
195214
const messages = await this.getMessages(request.session);
196215

197216
if (systemMessageDescription) {
@@ -255,8 +274,17 @@ export abstract class AbstractChatAgent implements ChatAgent {
255274
if (this.systemPromptId === undefined) {
256275
return undefined;
257276
}
277+
278+
const effectiveVariantId = this.promptService.getEffectiveVariantId(this.systemPromptId) ?? this.systemPromptId;
279+
const isEdited = this.isPromptVariantCustomized(effectiveVariantId);
280+
258281
const resolvedPrompt = await this.promptService.getResolvedPromptFragment(this.systemPromptId, undefined, context);
259-
return resolvedPrompt ? SystemMessageDescription.fromResolvedPromptFragment(resolvedPrompt) : undefined;
282+
return resolvedPrompt ? SystemMessageDescription.fromResolvedPromptFragment(resolvedPrompt, effectiveVariantId, isEdited) : undefined;
283+
}
284+
285+
protected isPromptVariantCustomized(fragmentId: string): boolean {
286+
const fragment = this.promptService.getRawPromptFragment(fragmentId);
287+
return fragment ? isCustomizedPromptFragment(fragment) : false;
260288
}
261289

262290
protected async getMessages(

packages/ai-chat/src/common/chat-model-serialization.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ export interface SerializableChatResponseData {
6262
isComplete: boolean;
6363
isError: boolean;
6464
errorMessage?: string;
65+
promptVariantId?: string;
66+
isPromptVariantEdited?: boolean;
6567
content: SerializableChatResponseContentData[];
6668
}
6769

packages/ai-chat/src/common/chat-model.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,14 @@ export interface ChatResponseModel {
743743
* This can be used to store and retrieve such data.
744744
*/
745745
readonly data: { [key: string]: unknown };
746+
/**
747+
* The ID of the prompt variant used to generate this response
748+
*/
749+
readonly promptVariantId?: string;
750+
/**
751+
* Indicates whether the prompt variant was customized/edited
752+
*/
753+
readonly isPromptVariantEdited?: boolean;
746754
toSerializable(): SerializableChatResponseData;
747755
}
748756

@@ -2331,6 +2339,8 @@ export class MutableChatResponseModel implements ChatResponseModel {
23312339
protected _isError: boolean;
23322340
protected _errorObject: Error | undefined;
23332341
protected _cancellationToken: CancellationTokenSource;
2342+
protected _promptVariantId?: string;
2343+
protected _isPromptVariantEdited?: boolean;
23342344

23352345
constructor(
23362346
requestId: string,
@@ -2372,6 +2382,8 @@ export class MutableChatResponseModel implements ChatResponseModel {
23722382
this._isWaitingForInput = false;
23732383
// TODO: Restore progressMessages?
23742384
this._progressMessages = [];
2385+
this._promptVariantId = data.promptVariantId;
2386+
this._isPromptVariantEdited = data.isPromptVariantEdited ?? false;
23752387

23762388
if (data.errorMessage) {
23772389
this._errorObject = new Error(data.errorMessage);
@@ -2443,6 +2455,20 @@ export class MutableChatResponseModel implements ChatResponseModel {
24432455
return this._agentId;
24442456
}
24452457

2458+
get promptVariantId(): string | undefined {
2459+
return this._promptVariantId;
2460+
}
2461+
2462+
get isPromptVariantEdited(): boolean {
2463+
return this._isPromptVariantEdited ?? false;
2464+
}
2465+
2466+
setPromptVariantInfo(variantId: string | undefined, isEdited: boolean): void {
2467+
this._promptVariantId = variantId;
2468+
this._isPromptVariantEdited = isEdited;
2469+
this._onDidChangeEmitter.fire();
2470+
}
2471+
24462472
overrideAgentId(agentId: string): void {
24472473
this._agentId = agentId;
24482474
}
@@ -2508,6 +2534,8 @@ export class MutableChatResponseModel implements ChatResponseModel {
25082534
isComplete: this.isComplete,
25092535
isError: this.isError,
25102536
errorMessage: this.errorObject?.message,
2537+
promptVariantId: this._promptVariantId,
2538+
isPromptVariantEdited: this._isPromptVariantEdited,
25112539
content: this.response.content.map(c => {
25122540
const serialized = c.toSerializable?.();
25132541
if (!serialized) {

0 commit comments

Comments
 (0)