@theia/ai-chat-ui 1.55.1 → 1.57.0-next.112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/lib/browser/ai-chat-ui-contribution.js +1 -1
- package/lib/browser/ai-chat-ui-contribution.js.map +1 -1
- package/lib/browser/ai-chat-ui-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-chat-ui-frontend-module.js +11 -1
- package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -1
- package/lib/browser/chat-input-widget.d.ts +18 -5
- package/lib/browser/chat-input-widget.d.ts.map +1 -1
- package/lib/browser/chat-input-widget.js +239 -79
- package/lib/browser/chat-input-widget.js.map +1 -1
- package/lib/browser/chat-response-renderer/code-part-renderer.d.ts +30 -2
- package/lib/browser/chat-response-renderer/code-part-renderer.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/code-part-renderer.js +45 -10
- package/lib/browser/chat-response-renderer/code-part-renderer.js.map +1 -1
- package/lib/browser/chat-response-renderer/markdown-part-renderer.d.ts +10 -4
- package/lib/browser/chat-response-renderer/markdown-part-renderer.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/markdown-part-renderer.js +41 -11
- package/lib/browser/chat-response-renderer/markdown-part-renderer.js.map +1 -1
- package/lib/browser/chat-response-renderer/question-part-renderer.d.ts +10 -0
- package/lib/browser/chat-response-renderer/question-part-renderer.d.ts.map +1 -0
- package/lib/browser/chat-response-renderer/question-part-renderer.js +43 -0
- package/lib/browser/chat-response-renderer/question-part-renderer.js.map +1 -0
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts +2 -0
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +38 -12
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
- package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts +6 -1
- package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts.map +1 -1
- package/lib/browser/chat-tree-view/chat-view-tree-widget.js +85 -14
- package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
- package/lib/browser/chat-view-language-contribution.d.ts.map +1 -1
- package/lib/browser/chat-view-language-contribution.js +0 -1
- package/lib/browser/chat-view-language-contribution.js.map +1 -1
- package/lib/browser/chat-view-widget.d.ts +4 -1
- package/lib/browser/chat-view-widget.d.ts.map +1 -1
- package/lib/browser/chat-view-widget.js +14 -4
- package/lib/browser/chat-view-widget.js.map +1 -1
- package/package.json +12 -12
- package/src/browser/ai-chat-ui-contribution.ts +1 -1
- package/src/browser/ai-chat-ui-frontend-module.ts +29 -5
- package/src/browser/chat-input-widget.tsx +351 -99
- package/src/browser/chat-response-renderer/code-part-renderer.tsx +48 -9
- package/src/browser/chat-response-renderer/markdown-part-renderer.tsx +42 -13
- package/src/browser/chat-response-renderer/question-part-renderer.tsx +59 -0
- package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +46 -11
- package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +141 -12
- package/src/browser/chat-view-language-contribution.ts +0 -1
- package/src/browser/chat-view-widget.tsx +19 -6
- package/src/browser/style/index.css +243 -22
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
|
|
18
|
-
import { injectable } from '@theia/core/shared/inversify';
|
|
18
|
+
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
19
19
|
import {
|
|
20
20
|
ChatResponseContent,
|
|
21
21
|
InformationalChatResponseContent,
|
|
@@ -26,9 +26,12 @@ import * as React from '@theia/core/shared/react';
|
|
|
26
26
|
import * as markdownit from '@theia/core/shared/markdown-it';
|
|
27
27
|
import * as DOMPurify from '@theia/core/shared/dompurify';
|
|
28
28
|
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
|
|
29
|
+
import { OpenerService, open } from '@theia/core/lib/browser';
|
|
30
|
+
import { URI } from '@theia/core';
|
|
29
31
|
|
|
30
32
|
@injectable()
|
|
31
33
|
export class MarkdownPartRenderer implements ChatResponsePartRenderer<MarkdownChatResponseContent | InformationalChatResponseContent> {
|
|
34
|
+
@inject(OpenerService) protected readonly openerService: OpenerService;
|
|
32
35
|
protected readonly markdownIt = markdownit();
|
|
33
36
|
canHandle(response: ChatResponseContent): number {
|
|
34
37
|
if (MarkdownChatResponseContent.is(response)) {
|
|
@@ -47,13 +50,12 @@ export class MarkdownPartRenderer implements ChatResponsePartRenderer<MarkdownCh
|
|
|
47
50
|
return null;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
|
-
return <MarkdownRender response={response} />;
|
|
53
|
+
return <MarkdownRender response={response} openerService={this.openerService} />;
|
|
51
54
|
}
|
|
52
|
-
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
const MarkdownRender = ({ response }: { response: MarkdownChatResponseContent | InformationalChatResponseContent }) => {
|
|
56
|
-
const ref = useMarkdownRendering(response.content);
|
|
57
|
+
const MarkdownRender = ({ response, openerService }: { response: MarkdownChatResponseContent | InformationalChatResponseContent; openerService: OpenerService }) => {
|
|
58
|
+
const ref = useMarkdownRendering(response.content, openerService);
|
|
57
59
|
|
|
58
60
|
return <div ref={ref}></div>;
|
|
59
61
|
};
|
|
@@ -61,30 +63,57 @@ const MarkdownRender = ({ response }: { response: MarkdownChatResponseContent |
|
|
|
61
63
|
/**
|
|
62
64
|
* This hook uses markdown-it directly to render markdown.
|
|
63
65
|
* The reason to use markdown-it directly is that the MarkdownRenderer is
|
|
64
|
-
*
|
|
65
|
-
* tags from the markdown with empty content.
|
|
66
|
-
*
|
|
66
|
+
* overridden by theia with a monaco version. This monaco version strips all html
|
|
67
|
+
* tags from the markdown with empty content. This leads to unexpected behavior when
|
|
68
|
+
* rendering markdown with html tags.
|
|
69
|
+
*
|
|
70
|
+
* Moreover, we want to intercept link clicks to use the Theia OpenerService instead of the default browser behavior.
|
|
67
71
|
*
|
|
68
72
|
* @param markdown the string to render as markdown
|
|
73
|
+
* @param skipSurroundingParagraph whether to remove a surrounding paragraph element (default: false)
|
|
74
|
+
* @param openerService the service to handle link opening
|
|
69
75
|
* @returns the ref to use in an element to render the markdown
|
|
70
76
|
*/
|
|
71
|
-
export const useMarkdownRendering = (markdown: string | MarkdownString) => {
|
|
77
|
+
export const useMarkdownRendering = (markdown: string | MarkdownString, openerService: OpenerService, skipSurroundingParagraph: boolean = false) => {
|
|
78
|
+
// null is valid in React
|
|
72
79
|
// eslint-disable-next-line no-null/no-null
|
|
73
80
|
const ref = useRef<HTMLDivElement | null>(null);
|
|
74
81
|
const markdownString = typeof markdown === 'string' ? markdown : markdown.value;
|
|
75
82
|
useEffect(() => {
|
|
76
83
|
const markdownIt = markdownit();
|
|
77
84
|
const host = document.createElement('div');
|
|
78
|
-
|
|
85
|
+
|
|
86
|
+
// markdownIt always puts the content in a paragraph element, so we remove it if we don't want that
|
|
87
|
+
const html = skipSurroundingParagraph ? markdownIt.render(markdownString).replace(/^<p>|<\/p>|<p><\/p>$/g, '') : markdownIt.render(markdownString);
|
|
88
|
+
|
|
79
89
|
host.innerHTML = DOMPurify.sanitize(html, {
|
|
80
|
-
|
|
90
|
+
// DOMPurify usually strips non http(s) links from hrefs
|
|
91
|
+
// but we want to allow them (see handleClick via OpenerService below)
|
|
92
|
+
ALLOW_UNKNOWN_PROTOCOLS: true
|
|
81
93
|
});
|
|
82
94
|
while (ref?.current?.firstChild) {
|
|
83
95
|
ref.current.removeChild(ref.current.firstChild);
|
|
84
96
|
}
|
|
85
|
-
|
|
86
97
|
ref?.current?.appendChild(host);
|
|
87
|
-
|
|
98
|
+
|
|
99
|
+
// intercept link clicks to use the Theia OpenerService instead of the default browser behavior
|
|
100
|
+
const handleClick = (event: MouseEvent) => {
|
|
101
|
+
let target = event.target as HTMLElement;
|
|
102
|
+
while (target && target.tagName !== 'A') {
|
|
103
|
+
target = target.parentElement as HTMLElement;
|
|
104
|
+
}
|
|
105
|
+
if (target && target.tagName === 'A') {
|
|
106
|
+
const href = target.getAttribute('href');
|
|
107
|
+
if (href) {
|
|
108
|
+
open(openerService, new URI(href));
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
ref?.current?.addEventListener('click', handleClick);
|
|
115
|
+
return () => ref.current?.removeEventListener('click', handleClick);
|
|
116
|
+
}, [markdownString, skipSurroundingParagraph, openerService]);
|
|
88
117
|
|
|
89
118
|
return ref;
|
|
90
119
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2024 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 { ChatResponseContent, QuestionResponseContent } from '@theia/ai-chat';
|
|
17
|
+
import { injectable } from '@theia/core/shared/inversify';
|
|
18
|
+
import * as React from '@theia/core/shared/react';
|
|
19
|
+
import { ReactNode } from '@theia/core/shared/react';
|
|
20
|
+
import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
|
|
21
|
+
import { ResponseNode } from '../chat-tree-view';
|
|
22
|
+
|
|
23
|
+
@injectable()
|
|
24
|
+
export class QuestionPartRenderer
|
|
25
|
+
implements ChatResponsePartRenderer<QuestionResponseContent> {
|
|
26
|
+
|
|
27
|
+
canHandle(response: ChatResponseContent): number {
|
|
28
|
+
if (QuestionResponseContent.is(response)) {
|
|
29
|
+
return 10;
|
|
30
|
+
}
|
|
31
|
+
return -1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
render(question: QuestionResponseContent, node: ResponseNode): ReactNode {
|
|
35
|
+
return (
|
|
36
|
+
<div className="theia-QuestionPartRenderer-root">
|
|
37
|
+
<div className="theia-QuestionPartRenderer-question">{question.question}</div>
|
|
38
|
+
<div className="theia-QuestionPartRenderer-options">
|
|
39
|
+
{
|
|
40
|
+
question.options.map((option, index) => (
|
|
41
|
+
<button
|
|
42
|
+
className={`theia-button theia-QuestionPartRenderer-option ${question.selectedOption === option ? 'selected' : ''}`}
|
|
43
|
+
onClick={() => {
|
|
44
|
+
question.selectedOption = option;
|
|
45
|
+
question.handler(option);
|
|
46
|
+
}}
|
|
47
|
+
disabled={question.selectedOption !== undefined || !node.response.isWaitingForInput}
|
|
48
|
+
key={index}
|
|
49
|
+
>
|
|
50
|
+
{option.text}
|
|
51
|
+
</button>
|
|
52
|
+
))
|
|
53
|
+
}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
}
|
|
@@ -29,26 +29,61 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
|
|
|
29
29
|
}
|
|
30
30
|
return -1;
|
|
31
31
|
}
|
|
32
|
+
|
|
32
33
|
render(response: ToolCallChatResponseContent): ReactNode {
|
|
33
|
-
return
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
return (
|
|
35
|
+
<h4 className='theia-toolCall'>
|
|
36
|
+
{response.finished ? (
|
|
37
|
+
<details>
|
|
38
|
+
<summary>Ran {response.name}
|
|
39
|
+
({this.renderCollapsibleArguments(response.arguments)})
|
|
40
|
+
</summary>
|
|
41
|
+
<pre>{this.tryPrettyPrintJson(response)}</pre>
|
|
42
|
+
</details>
|
|
43
|
+
) : (
|
|
44
|
+
<span>
|
|
45
|
+
<Spinner /> Running {response.name}({this.renderCollapsibleArguments(response.arguments)})
|
|
46
|
+
</span>
|
|
47
|
+
)}
|
|
48
|
+
</h4>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
protected renderCollapsibleArguments(args: string | undefined): ReactNode {
|
|
53
|
+
if (!args || !args.trim() || args.trim() === '{}') {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<details className="collapsible-arguments">
|
|
59
|
+
<summary className="collapsible-arguments-summary">...</summary>
|
|
60
|
+
<span>{this.prettyPrintArgs(args)}</span>
|
|
61
|
+
</details>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
42
64
|
|
|
65
|
+
private prettyPrintArgs(args: string): string {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.stringify(JSON.parse(args), undefined, 2);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// fall through
|
|
70
|
+
return args;
|
|
71
|
+
}
|
|
43
72
|
}
|
|
44
73
|
|
|
45
74
|
private tryPrettyPrintJson(response: ToolCallChatResponseContent): string | undefined {
|
|
46
75
|
let responseContent = response.result;
|
|
47
76
|
try {
|
|
48
|
-
if (
|
|
49
|
-
responseContent
|
|
77
|
+
if (responseContent) {
|
|
78
|
+
if (typeof responseContent === 'string') {
|
|
79
|
+
responseContent = JSON.parse(responseContent);
|
|
80
|
+
}
|
|
81
|
+
responseContent = JSON.stringify(responseContent, undefined, 2);
|
|
50
82
|
}
|
|
51
83
|
} catch (e) {
|
|
84
|
+
if (typeof responseContent !== 'string') {
|
|
85
|
+
responseContent = `The content could not be converted to string: '${e.message}'. This is the original content: '${responseContent}'.`;
|
|
86
|
+
}
|
|
52
87
|
// fall through
|
|
53
88
|
}
|
|
54
89
|
return responseContent;
|
|
@@ -14,12 +14,15 @@
|
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
import {
|
|
17
|
+
ChatAgent,
|
|
17
18
|
ChatAgentService,
|
|
18
19
|
ChatModel,
|
|
19
20
|
ChatProgressMessage,
|
|
20
21
|
ChatRequestModel,
|
|
21
22
|
ChatResponseContent,
|
|
22
23
|
ChatResponseModel,
|
|
24
|
+
ParsedChatRequestAgentPart,
|
|
25
|
+
ParsedChatRequestVariablePart,
|
|
23
26
|
} from '@theia/ai-chat';
|
|
24
27
|
import { CommandRegistry, ContributionProvider } from '@theia/core';
|
|
25
28
|
import {
|
|
@@ -27,9 +30,11 @@ import {
|
|
|
27
30
|
CommonCommands,
|
|
28
31
|
CompositeTreeNode,
|
|
29
32
|
ContextMenuRenderer,
|
|
33
|
+
HoverService,
|
|
30
34
|
Key,
|
|
31
35
|
KeyCode,
|
|
32
36
|
NodeProps,
|
|
37
|
+
OpenerService,
|
|
33
38
|
TreeModel,
|
|
34
39
|
TreeNode,
|
|
35
40
|
TreeProps,
|
|
@@ -46,6 +51,7 @@ import * as React from '@theia/core/shared/react';
|
|
|
46
51
|
import { ChatNodeToolbarActionContribution } from '../chat-node-toolbar-action-contribution';
|
|
47
52
|
import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
|
|
48
53
|
import { useMarkdownRendering } from '../chat-response-renderer/markdown-part-renderer';
|
|
54
|
+
import { AIVariableService } from '@theia/ai-core';
|
|
49
55
|
|
|
50
56
|
// TODO Instead of directly operating on the ChatRequestModel we could use an intermediate view model
|
|
51
57
|
export interface RequestNode extends TreeNode {
|
|
@@ -77,9 +83,18 @@ export class ChatViewTreeWidget extends TreeWidget {
|
|
|
77
83
|
@inject(ChatAgentService)
|
|
78
84
|
protected chatAgentService: ChatAgentService;
|
|
79
85
|
|
|
86
|
+
@inject(AIVariableService)
|
|
87
|
+
protected readonly variableService: AIVariableService;
|
|
88
|
+
|
|
80
89
|
@inject(CommandRegistry)
|
|
81
90
|
private commandRegistry: CommandRegistry;
|
|
82
91
|
|
|
92
|
+
@inject(OpenerService)
|
|
93
|
+
protected readonly openerService: OpenerService;
|
|
94
|
+
|
|
95
|
+
@inject(HoverService)
|
|
96
|
+
private hoverService: HoverService;
|
|
97
|
+
|
|
83
98
|
protected _shouldScrollToEnd = true;
|
|
84
99
|
|
|
85
100
|
protected isEnabled = false;
|
|
@@ -267,22 +282,39 @@ export class ChatViewTreeWidget extends TreeWidget {
|
|
|
267
282
|
|
|
268
283
|
private renderAgent(node: RequestNode | ResponseNode): React.ReactNode {
|
|
269
284
|
const inProgress = isResponseNode(node) && !node.response.isComplete && !node.response.isCanceled && !node.response.isError;
|
|
285
|
+
const waitingForInput = isResponseNode(node) && node.response.isWaitingForInput;
|
|
270
286
|
const toolbarContributions = !inProgress
|
|
271
287
|
? this.chatNodeToolbarActionContributions.getContributions()
|
|
272
288
|
.flatMap(c => c.getToolbarActions(node))
|
|
273
289
|
.filter(action => this.commandRegistry.isEnabled(action.commandId, node))
|
|
274
290
|
.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0))
|
|
275
291
|
: [];
|
|
292
|
+
const agentLabel = React.createRef<HTMLHeadingElement>();
|
|
293
|
+
const agentDescription = this.getAgent(node)?.description;
|
|
276
294
|
return <React.Fragment>
|
|
277
295
|
<div className='theia-ChatNodeHeader'>
|
|
278
296
|
<div className={`theia-AgentAvatar ${this.getAgentIconClassName(node)}`}></div>
|
|
279
|
-
<h3
|
|
280
|
-
|
|
297
|
+
<h3 ref={agentLabel}
|
|
298
|
+
className='theia-AgentLabel'
|
|
299
|
+
onMouseEnter={() => {
|
|
300
|
+
if (agentDescription) {
|
|
301
|
+
this.hoverService.requestHover({
|
|
302
|
+
content: agentDescription,
|
|
303
|
+
target: agentLabel.current!,
|
|
304
|
+
position: 'right'
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}}>
|
|
308
|
+
{this.getAgentLabel(node)}
|
|
309
|
+
</h3>
|
|
310
|
+
{inProgress && !waitingForInput && <span className='theia-ChatContentInProgress'>Generating</span>}
|
|
311
|
+
{inProgress && waitingForInput && <span className='theia-ChatContentInProgress'>Waiting for input</span>}
|
|
281
312
|
<div className='theia-ChatNodeToolbar'>
|
|
282
313
|
{!inProgress &&
|
|
283
314
|
toolbarContributions.length > 0 &&
|
|
284
315
|
toolbarContributions.map(action =>
|
|
285
316
|
<span
|
|
317
|
+
key={action.commandId}
|
|
286
318
|
className={`theia-ChatNodeToolbarAction ${action.icon}`}
|
|
287
319
|
title={action.tooltip}
|
|
288
320
|
onClick={e => {
|
|
@@ -308,8 +340,14 @@ export class ChatViewTreeWidget extends TreeWidget {
|
|
|
308
340
|
// TODO find user name
|
|
309
341
|
return 'You';
|
|
310
342
|
}
|
|
311
|
-
|
|
312
|
-
|
|
343
|
+
return this.getAgent(node)?.name ?? 'AI';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private getAgent(node: RequestNode | ResponseNode): ChatAgent | undefined {
|
|
347
|
+
if (isRequestNode(node)) {
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
return node.response.agentId ? this.chatAgentService.getAgent(node.response.agentId) : undefined;
|
|
313
351
|
}
|
|
314
352
|
|
|
315
353
|
private getAgentIconClassName(node: RequestNode | ResponseNode): string | undefined {
|
|
@@ -331,7 +369,13 @@ export class ChatViewTreeWidget extends TreeWidget {
|
|
|
331
369
|
}
|
|
332
370
|
|
|
333
371
|
private renderChatRequest(node: RequestNode): React.ReactNode {
|
|
334
|
-
return <ChatRequestRender
|
|
372
|
+
return <ChatRequestRender
|
|
373
|
+
node={node}
|
|
374
|
+
hoverService={this.hoverService}
|
|
375
|
+
chatAgentService={this.chatAgentService}
|
|
376
|
+
variableService={this.variableService}
|
|
377
|
+
openerService={this.openerService}
|
|
378
|
+
/>;
|
|
335
379
|
}
|
|
336
380
|
|
|
337
381
|
private renderChatResponse(node: ResponseNode): React.ReactNode {
|
|
@@ -339,12 +383,28 @@ export class ChatViewTreeWidget extends TreeWidget {
|
|
|
339
383
|
<div className={'theia-ResponseNode'}>
|
|
340
384
|
{!node.response.isComplete
|
|
341
385
|
&& node.response.response.content.length === 0
|
|
342
|
-
&& node.response.progressMessages
|
|
343
|
-
|
|
344
|
-
|
|
386
|
+
&& node.response.progressMessages
|
|
387
|
+
.filter(c => c.show === 'untilFirstContent')
|
|
388
|
+
.map((c, i) =>
|
|
389
|
+
<ProgressMessage {...c} key={`${node.id}-progress-untilFirstContent-${i}`} />
|
|
390
|
+
)
|
|
391
|
+
}
|
|
345
392
|
{node.response.response.content.map((c, i) =>
|
|
346
393
|
<div className='theia-ResponseNode-Content' key={`${node.id}-content-${i}`}>{this.getChatResponsePartRenderer(c, node)}</div>
|
|
347
394
|
)}
|
|
395
|
+
{!node.response.isComplete
|
|
396
|
+
&& node.response.progressMessages
|
|
397
|
+
.filter(c => c.show === 'whileIncomplete')
|
|
398
|
+
.map((c, i) =>
|
|
399
|
+
<ProgressMessage {...c} key={`${node.id}-progress-whileIncomplete-${i}`} />
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
{node.response.progressMessages
|
|
403
|
+
.filter(c => c.show === 'forever')
|
|
404
|
+
.map((c, i) =>
|
|
405
|
+
<ProgressMessage {...c} key={`${node.id}-progress-afterComplete-${i}`} />
|
|
406
|
+
)
|
|
407
|
+
}
|
|
348
408
|
</div>
|
|
349
409
|
);
|
|
350
410
|
}
|
|
@@ -375,11 +435,80 @@ export class ChatViewTreeWidget extends TreeWidget {
|
|
|
375
435
|
}
|
|
376
436
|
}
|
|
377
437
|
|
|
378
|
-
const ChatRequestRender = (
|
|
379
|
-
|
|
380
|
-
|
|
438
|
+
const ChatRequestRender = (
|
|
439
|
+
{
|
|
440
|
+
node, hoverService, chatAgentService, variableService, openerService
|
|
441
|
+
}: {
|
|
442
|
+
node: RequestNode,
|
|
443
|
+
hoverService: HoverService,
|
|
444
|
+
chatAgentService: ChatAgentService,
|
|
445
|
+
variableService: AIVariableService,
|
|
446
|
+
openerService: OpenerService
|
|
447
|
+
}) => {
|
|
448
|
+
const parts = node.request.message.parts;
|
|
449
|
+
return (
|
|
450
|
+
<div className="theia-RequestNode">
|
|
451
|
+
<p>
|
|
452
|
+
{parts.map((part, index) => {
|
|
453
|
+
if (part instanceof ParsedChatRequestAgentPart || part instanceof ParsedChatRequestVariablePart) {
|
|
454
|
+
let description = undefined;
|
|
455
|
+
let className = '';
|
|
456
|
+
if (part instanceof ParsedChatRequestAgentPart) {
|
|
457
|
+
description = chatAgentService.getAgent(part.agentId)?.description;
|
|
458
|
+
className = 'theia-RequestNode-AgentLabel';
|
|
459
|
+
} else if (part instanceof ParsedChatRequestVariablePart) {
|
|
460
|
+
description = variableService.getVariable(part.variableName)?.description;
|
|
461
|
+
className = 'theia-RequestNode-VariableLabel';
|
|
462
|
+
}
|
|
463
|
+
return (
|
|
464
|
+
<HoverableLabel
|
|
465
|
+
key={index}
|
|
466
|
+
text={part.text}
|
|
467
|
+
description={description}
|
|
468
|
+
hoverService={hoverService}
|
|
469
|
+
className={className}
|
|
470
|
+
/>
|
|
471
|
+
);
|
|
472
|
+
} else {
|
|
473
|
+
// maintain the leading and trailing spaces with explicit ` `, otherwise they would get trimmed by the markdown renderer
|
|
474
|
+
const ref = useMarkdownRendering(part.text.replace(/^\s|\s$/g, ' '), openerService, true);
|
|
475
|
+
return (
|
|
476
|
+
<span key={index} ref={ref}></span>
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
})}
|
|
480
|
+
</p>
|
|
481
|
+
</div>
|
|
482
|
+
);
|
|
483
|
+
};
|
|
381
484
|
|
|
382
|
-
|
|
485
|
+
const HoverableLabel = (
|
|
486
|
+
{
|
|
487
|
+
text, description, hoverService, className
|
|
488
|
+
}: {
|
|
489
|
+
text: string,
|
|
490
|
+
description?: string,
|
|
491
|
+
hoverService: HoverService,
|
|
492
|
+
className: string
|
|
493
|
+
}) => {
|
|
494
|
+
const spanRef = React.createRef<HTMLSpanElement>();
|
|
495
|
+
return (
|
|
496
|
+
<span
|
|
497
|
+
className={className}
|
|
498
|
+
ref={spanRef}
|
|
499
|
+
onMouseEnter={() => {
|
|
500
|
+
if (description) {
|
|
501
|
+
hoverService.requestHover({
|
|
502
|
+
content: description,
|
|
503
|
+
target: spanRef.current!,
|
|
504
|
+
position: 'right'
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}}
|
|
508
|
+
>
|
|
509
|
+
{text}
|
|
510
|
+
</span>
|
|
511
|
+
);
|
|
383
512
|
};
|
|
384
513
|
|
|
385
514
|
const ProgressMessage = (c: ChatProgressMessage) => (
|
|
@@ -39,7 +39,6 @@ export class ChatViewLanguageContribution implements FrontendApplicationContribu
|
|
|
39
39
|
private providers: ContributionProvider<ToolProvider>;
|
|
40
40
|
|
|
41
41
|
onStart(_app: FrontendApplication): MaybePromise<void> {
|
|
42
|
-
console.log('ChatViewLanguageContribution started');
|
|
43
42
|
monaco.languages.register({ id: CHAT_VIEW_LANGUAGE_ID, extensions: [CHAT_VIEW_LANGUAGE_EXTENSION] });
|
|
44
43
|
|
|
45
44
|
monaco.languages.registerCompletionItemProvider(CHAT_VIEW_LANGUAGE_ID, {
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
import { CommandService, deepClone, Emitter, Event, MessageService } from '@theia/core';
|
|
17
|
-
import { ChatRequest, ChatRequestModel,
|
|
18
|
-
import { BaseWidget, codicon, ExtractableWidget, PanelLayout, PreferenceService, StatefulWidget } from '@theia/core/lib/browser';
|
|
17
|
+
import { ChatRequest, ChatRequestModel, ChatService, ChatSession } from '@theia/ai-chat';
|
|
18
|
+
import { BaseWidget, codicon, ExtractableWidget, Message, PanelLayout, PreferenceService, StatefulWidget } from '@theia/core/lib/browser';
|
|
19
19
|
import { nls } from '@theia/core/lib/common/nls';
|
|
20
20
|
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
|
21
21
|
import { AIChatInputWidget } from './chat-input-widget';
|
|
@@ -93,6 +93,8 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
|
|
|
93
93
|
this.inputWidget.onQuery = this.onQuery.bind(this);
|
|
94
94
|
this.inputWidget.onCancel = this.onCancel.bind(this);
|
|
95
95
|
this.inputWidget.chatModel = this.chatSession.model;
|
|
96
|
+
this.inputWidget.onDeleteChangeSet = this.onDeleteChangeSet.bind(this);
|
|
97
|
+
this.inputWidget.onDeleteChangeSetElement = this.onDeleteChangeSetElement.bind(this);
|
|
96
98
|
this.treeWidget.trackChatModel(this.chatSession.model);
|
|
97
99
|
|
|
98
100
|
this.initListeners();
|
|
@@ -125,6 +127,11 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
|
|
|
125
127
|
);
|
|
126
128
|
}
|
|
127
129
|
|
|
130
|
+
protected override onActivateRequest(msg: Message): void {
|
|
131
|
+
super.onActivateRequest(msg);
|
|
132
|
+
this.inputWidget.activate();
|
|
133
|
+
}
|
|
134
|
+
|
|
128
135
|
storeState(): object {
|
|
129
136
|
return this.state;
|
|
130
137
|
}
|
|
@@ -160,7 +167,7 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
|
|
|
160
167
|
const requestProgress = await this.chatService.sendRequest(this.chatSession.id, chatRequest);
|
|
161
168
|
requestProgress?.responseCompleted.then(responseModel => {
|
|
162
169
|
if (responseModel.isError) {
|
|
163
|
-
this.messageService.error(responseModel.errorObject?.message ?? 'An error occurred
|
|
170
|
+
this.messageService.error(responseModel.errorObject?.message ?? 'An error occurred during chat service invocation.');
|
|
164
171
|
}
|
|
165
172
|
});
|
|
166
173
|
if (!requestProgress) {
|
|
@@ -171,9 +178,15 @@ export class ChatViewWidget extends BaseWidget implements ExtractableWidget, Sta
|
|
|
171
178
|
}
|
|
172
179
|
|
|
173
180
|
protected onCancel(requestModel: ChatRequestModel): void {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
181
|
+
this.chatService.cancelRequest(requestModel.session.id, requestModel.id);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
protected onDeleteChangeSet(sessionId: string): void {
|
|
185
|
+
this.chatService.deleteChangeSet(sessionId);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
protected onDeleteChangeSetElement(sessionId: string, index: number): void {
|
|
189
|
+
this.chatService.deleteChangeSetElement(sessionId, index);
|
|
177
190
|
}
|
|
178
191
|
|
|
179
192
|
lock(): void {
|