Skip to content

Commit da2388d

Browse files
committed
fix(ai-chat-ui): Highlight referenced tools in users' chat messages
Tool function references (~functionName) in chat messages are now displayed with badge styling matching agents and variables, with hover tooltips showing tool descriptions. - Add rendering support for ParsedChatRequestFunctionPart in ChatRequestRender - Extend CSS selector to include .theia-RequestNode-FunctionLabel - Fix kind property assignments in ParsedChatRequest*Part classes - Add tests for kind property runtime assignments Fixes #16722
1 parent 304d87b commit da2388d

File tree

3 files changed

+75
-33
lines changed

3 files changed

+75
-33
lines changed

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
ChatService,
2424
EditableChatRequestModel,
2525
ParsedChatRequestAgentPart,
26+
ParsedChatRequestFunctionPart,
2627
ParsedChatRequestVariablePart,
2728
type ChatRequest,
2829
type ChatHierarchyBranch,
@@ -808,7 +809,7 @@ const ChatRequestRender = (
808809
<div className="theia-RequestNode">
809810
<p>
810811
{parts.map((part, index) => {
811-
if (part instanceof ParsedChatRequestAgentPart || part instanceof ParsedChatRequestVariablePart) {
812+
if (part instanceof ParsedChatRequestAgentPart || part instanceof ParsedChatRequestVariablePart || part instanceof ParsedChatRequestFunctionPart) {
812813
let description = undefined;
813814
let className = '';
814815
if (part instanceof ParsedChatRequestAgentPart) {
@@ -817,6 +818,9 @@ const ChatRequestRender = (
817818
} else if (part instanceof ParsedChatRequestVariablePart) {
818819
description = variableService.getVariable(part.variableName)?.description;
819820
className = 'theia-RequestNode-VariableLabel';
821+
} else if (part instanceof ParsedChatRequestFunctionPart) {
822+
description = part.toolRequest?.description;
823+
className = 'theia-RequestNode-FunctionLabel';
820824
}
821825
return (
822826
<HoverableLabel

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ div:last-child > .theia-ChatNode {
156156
}
157157

158158
.theia-RequestNode .theia-RequestNode-AgentLabel,
159-
.theia-RequestNode .theia-RequestNode-VariableLabel {
159+
.theia-RequestNode .theia-RequestNode-VariableLabel,
160+
.theia-RequestNode .theia-RequestNode-FunctionLabel {
160161
padding: calc(var(--theia-ui-padding) * 2 / 3);
161162
padding-top: 0px;
162163
padding-bottom: 0px;

packages/ai-chat/src/common/chat-request-parser.spec.ts

Lines changed: 68 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { ChatContext, ChatRequest } from './chat-model';
2222
import { expect } from 'chai';
2323
import { AIVariable, DefaultAIVariableService, ResolvedAIVariable, ToolInvocationRegistryImpl, ToolRequest } from '@theia/ai-core';
2424
import { ILogger, Logger } from '@theia/core';
25-
import { ParsedChatRequestTextPart, ParsedChatRequestVariablePart } from './parsed-chat-request';
25+
import { ParsedChatRequestAgentPart, ParsedChatRequestFunctionPart, ParsedChatRequestTextPart, ParsedChatRequestVariablePart } from './parsed-chat-request';
2626

2727
describe('ChatRequestParserImpl', () => {
2828
const chatAgentService = sinon.createStubInstance(ChatAgentServiceImpl);
@@ -42,10 +42,11 @@ describe('ChatRequestParserImpl', () => {
4242
};
4343
const context: ChatContext = { variables: [] };
4444
const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);
45-
expect(result.parts).to.deep.contain({
46-
text: 'What is the best pizza topping?',
47-
range: { start: 0, endExclusive: 31 }
48-
});
45+
expect(result.parts.length).to.equal(1);
46+
const part = result.parts[0] as ParsedChatRequestTextPart;
47+
expect(part.kind).to.equal('text');
48+
expect(part.text).to.equal('What is the best pizza topping?');
49+
expect(part.range).to.deep.equal({ start: 0, endExclusive: 31 });
4950
});
5051

5152
it('parses text with variable name', async () => {
@@ -54,19 +55,23 @@ describe('ChatRequestParserImpl', () => {
5455
};
5556
const context: ChatContext = { variables: [] };
5657
const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);
57-
expect(result).to.deep.contain({
58-
parts: [{
59-
text: 'What is the ',
60-
range: { start: 0, endExclusive: 12 }
61-
}, {
62-
variableName: 'best',
63-
variableArg: undefined,
64-
range: { start: 12, endExclusive: 17 }
65-
}, {
66-
text: ' pizza topping?',
67-
range: { start: 17, endExclusive: 32 }
68-
}]
69-
});
58+
expect(result.parts.length).to.equal(3);
59+
60+
const textPart1 = result.parts[0] as ParsedChatRequestTextPart;
61+
expect(textPart1.kind).to.equal('text');
62+
expect(textPart1.text).to.equal('What is the ');
63+
expect(textPart1.range).to.deep.equal({ start: 0, endExclusive: 12 });
64+
65+
const varPart = result.parts[1] as ParsedChatRequestVariablePart;
66+
expect(varPart.kind).to.equal('var');
67+
expect(varPart.variableName).to.equal('best');
68+
expect(varPart.variableArg).to.be.undefined;
69+
expect(varPart.range).to.deep.equal({ start: 12, endExclusive: 17 });
70+
71+
const textPart2 = result.parts[2] as ParsedChatRequestTextPart;
72+
expect(textPart2.kind).to.equal('text');
73+
expect(textPart2.text).to.equal(' pizza topping?');
74+
expect(textPart2.range).to.deep.equal({ start: 17, endExclusive: 32 });
7075
});
7176

7277
it('parses text with variable name with argument', async () => {
@@ -75,19 +80,23 @@ describe('ChatRequestParserImpl', () => {
7580
};
7681
const context: ChatContext = { variables: [] };
7782
const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);
78-
expect(result).to.deep.contain({
79-
parts: [{
80-
text: 'What is the ',
81-
range: { start: 0, endExclusive: 12 }
82-
}, {
83-
variableName: 'best',
84-
variableArg: 'by-poll',
85-
range: { start: 12, endExclusive: 25 }
86-
}, {
87-
text: ' pizza topping?',
88-
range: { start: 25, endExclusive: 40 }
89-
}]
90-
});
83+
expect(result.parts.length).to.equal(3);
84+
85+
const textPart1 = result.parts[0] as ParsedChatRequestTextPart;
86+
expect(textPart1.kind).to.equal('text');
87+
expect(textPart1.text).to.equal('What is the ');
88+
expect(textPart1.range).to.deep.equal({ start: 0, endExclusive: 12 });
89+
90+
const varPart = result.parts[1] as ParsedChatRequestVariablePart;
91+
expect(varPart.kind).to.equal('var');
92+
expect(varPart.variableName).to.equal('best');
93+
expect(varPart.variableArg).to.equal('by-poll');
94+
expect(varPart.range).to.deep.equal({ start: 12, endExclusive: 25 });
95+
96+
const textPart2 = result.parts[2] as ParsedChatRequestTextPart;
97+
expect(textPart2.kind).to.equal('text');
98+
expect(textPart2.text).to.equal(' pizza topping?');
99+
expect(textPart2.range).to.deep.equal({ start: 25, endExclusive: 40 });
91100
});
92101

93102
it('parses text with variable name with numeric argument', async () => {
@@ -265,4 +274,32 @@ describe('ChatRequestParserImpl', () => {
265274
const varPart = result.parts[0] as ParsedChatRequestVariablePart;
266275
expect(varPart.variableArg).to.equal('cmd|"arg with \\"quote\\"" other');
267276
});
277+
278+
describe('parsed chat request part kind assignments', () => {
279+
it('ParsedChatRequestTextPart has kind assigned at runtime', () => {
280+
const part = new ParsedChatRequestTextPart({ start: 0, endExclusive: 5 }, 'hello');
281+
expect(part.kind).to.equal('text');
282+
});
283+
284+
it('ParsedChatRequestVariablePart has kind assigned at runtime', () => {
285+
const part = new ParsedChatRequestVariablePart({ start: 0, endExclusive: 5 }, 'varName', undefined);
286+
expect(part.kind).to.equal('var');
287+
});
288+
289+
it('ParsedChatRequestFunctionPart has kind assigned at runtime', () => {
290+
const toolRequest: ToolRequest = {
291+
id: 'testTool',
292+
name: 'Test Tool',
293+
handler: async () => undefined,
294+
parameters: { type: 'object', properties: {} }
295+
};
296+
const part = new ParsedChatRequestFunctionPart({ start: 0, endExclusive: 5 }, toolRequest);
297+
expect(part.kind).to.equal('function');
298+
});
299+
300+
it('ParsedChatRequestAgentPart has kind assigned at runtime', () => {
301+
const part = new ParsedChatRequestAgentPart({ start: 0, endExclusive: 5 }, 'agentId', 'agentName');
302+
expect(part.kind).to.equal('agent');
303+
});
304+
});
268305
});

0 commit comments

Comments
 (0)