@theia/ai-chat-ui 1.60.2 → 1.61.0
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/lib/browser/ai-chat-ui-contribution.d.ts +5 -1
- package/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -1
- package/lib/browser/ai-chat-ui-contribution.js +102 -4
- 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 +30 -1
- package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -1
- package/lib/browser/chat-input-agent-suggestions.d.ts +11 -0
- package/lib/browser/chat-input-agent-suggestions.d.ts.map +1 -0
- package/lib/browser/chat-input-agent-suggestions.js +76 -0
- package/lib/browser/chat-input-agent-suggestions.js.map +1 -0
- package/lib/browser/chat-input-widget.d.ts +17 -6
- package/lib/browser/chat-input-widget.d.ts.map +1 -1
- package/lib/browser/chat-input-widget.js +72 -22
- package/lib/browser/chat-input-widget.js.map +1 -1
- package/lib/browser/chat-node-toolbar-action-contribution.d.ts +8 -0
- package/lib/browser/chat-node-toolbar-action-contribution.d.ts.map +1 -1
- package/lib/browser/chat-node-toolbar-action-contribution.js +55 -1
- package/lib/browser/chat-node-toolbar-action-contribution.js.map +1 -1
- package/lib/browser/chat-progress-message.d.ts +7 -0
- package/lib/browser/chat-progress-message.d.ts.map +1 -0
- package/lib/browser/chat-progress-message.js +33 -0
- package/lib/browser/chat-progress-message.js.map +1 -0
- package/lib/browser/chat-response-renderer/code-part-renderer.js +1 -1
- package/lib/browser/chat-response-renderer/code-part-renderer.js.map +1 -1
- package/lib/browser/chat-response-renderer/index.d.ts +1 -0
- package/lib/browser/chat-response-renderer/index.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/index.js +1 -0
- package/lib/browser/chat-response-renderer/index.js.map +1 -1
- package/lib/browser/chat-response-renderer/markdown-part-renderer.d.ts +7 -1
- package/lib/browser/chat-response-renderer/markdown-part-renderer.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/markdown-part-renderer.js +14 -3
- package/lib/browser/chat-response-renderer/markdown-part-renderer.js.map +1 -1
- package/lib/browser/chat-response-renderer/progress-part-renderer.d.ts +9 -0
- package/lib/browser/chat-response-renderer/progress-part-renderer.d.ts.map +1 -0
- package/lib/browser/chat-response-renderer/progress-part-renderer.js +39 -0
- package/lib/browser/chat-response-renderer/progress-part-renderer.js.map +1 -0
- package/lib/browser/chat-tree-view/chat-view-tree-input-widget.d.ts +33 -0
- package/lib/browser/chat-tree-view/chat-view-tree-input-widget.d.ts.map +1 -0
- package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js +79 -0
- package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js.map +1 -0
- package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts +20 -4
- 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 +184 -48
- package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
- package/lib/browser/chat-view-commands.d.ts +3 -0
- package/lib/browser/chat-view-commands.d.ts.map +1 -1
- package/lib/browser/chat-view-commands.js +15 -0
- package/lib/browser/chat-view-commands.js.map +1 -1
- package/lib/browser/chat-view-contribution.d.ts +1 -0
- package/lib/browser/chat-view-contribution.d.ts.map +1 -1
- package/lib/browser/chat-view-contribution.js +16 -14
- package/lib/browser/chat-view-contribution.js.map +1 -1
- package/lib/browser/chat-view-language-contribution.d.ts +3 -3
- package/lib/browser/chat-view-language-contribution.d.ts.map +1 -1
- package/lib/browser/chat-view-language-contribution.js +9 -22
- package/lib/browser/chat-view-language-contribution.js.map +1 -1
- package/lib/browser/chat-view-widget.d.ts +6 -2
- package/lib/browser/chat-view-widget.d.ts.map +1 -1
- package/lib/browser/chat-view-widget.js +36 -19
- package/lib/browser/chat-view-widget.js.map +1 -1
- package/package.json +12 -12
- package/src/browser/ai-chat-ui-contribution.ts +93 -6
- package/src/browser/ai-chat-ui-frontend-module.ts +33 -3
- package/src/browser/chat-input-agent-suggestions.tsx +85 -0
- package/src/browser/chat-input-widget.tsx +122 -32
- package/src/browser/chat-node-toolbar-action-contribution.ts +40 -1
- package/src/browser/chat-progress-message.tsx +40 -0
- package/src/browser/chat-response-renderer/code-part-renderer.tsx +3 -3
- package/src/browser/chat-response-renderer/index.ts +1 -0
- package/src/browser/chat-response-renderer/markdown-part-renderer.tsx +19 -2
- package/src/browser/chat-response-renderer/progress-part-renderer.tsx +40 -0
- package/src/browser/chat-tree-view/chat-view-tree-input-widget.tsx +89 -0
- package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +200 -37
- package/src/browser/chat-view-commands.ts +18 -0
- package/src/browser/chat-view-contribution.ts +20 -16
- package/src/browser/chat-view-language-contribution.ts +10 -24
- package/src/browser/chat-view-widget.tsx +18 -5
- package/src/browser/style/index.css +58 -4
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 EclipseSource GmbH and others.
|
|
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
|
+
|
|
17
|
+
import * as React from '@theia/core/shared/react';
|
|
18
|
+
import { DeclaredEventsEventListenerObject, useMarkdownRendering } from './chat-response-renderer/markdown-part-renderer';
|
|
19
|
+
import { OpenerService } from '@theia/core/lib/browser';
|
|
20
|
+
import { ChatSuggestion, ChatSuggestionCallback } from '@theia/ai-chat';
|
|
21
|
+
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
|
|
22
|
+
|
|
23
|
+
interface ChatInputAgentSuggestionsProps {
|
|
24
|
+
suggestions: readonly ChatSuggestion[];
|
|
25
|
+
opener: OpenerService;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getText(suggestion: ChatSuggestion): string {
|
|
29
|
+
if (typeof suggestion === 'string') { return suggestion; }
|
|
30
|
+
if ('value' in suggestion) { return suggestion.value; }
|
|
31
|
+
if (typeof suggestion.content === 'string') { return suggestion.content; }
|
|
32
|
+
return suggestion.content.value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getContent(suggestion: ChatSuggestion): string | MarkdownString {
|
|
36
|
+
if (typeof suggestion === 'string') { return suggestion; }
|
|
37
|
+
if ('value' in suggestion) { return suggestion; }
|
|
38
|
+
return suggestion.content;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const ChatInputAgentSuggestions: React.FC<ChatInputAgentSuggestionsProps> = ({ suggestions, opener }) => (
|
|
42
|
+
!!suggestions?.length && <div className="chat-agent-suggestions">
|
|
43
|
+
{suggestions.map(suggestion => <ChatInputAgentSuggestion
|
|
44
|
+
key={getText(suggestion)}
|
|
45
|
+
suggestion={suggestion}
|
|
46
|
+
opener={opener}
|
|
47
|
+
handler={ChatSuggestionCallback.is(suggestion) ? new ChatSuggestionClickHandler(suggestion) : undefined}
|
|
48
|
+
/>)}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
interface ChatInputAgestSuggestionProps {
|
|
53
|
+
suggestion: ChatSuggestion;
|
|
54
|
+
opener: OpenerService;
|
|
55
|
+
handler?: DeclaredEventsEventListenerObject;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const ChatInputAgentSuggestion: React.FC<ChatInputAgestSuggestionProps> = ({ suggestion, opener, handler }) => {
|
|
59
|
+
const ref = useMarkdownRendering(getContent(suggestion), opener, true, handler);
|
|
60
|
+
return <div className="chat-agent-suggestion" style={(!handler || ChatSuggestionCallback.containsCallbackLink(suggestion)) ? undefined : { cursor: 'pointer' }} ref={ref} />;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
class ChatSuggestionClickHandler implements DeclaredEventsEventListenerObject {
|
|
64
|
+
constructor(protected readonly suggestion: ChatSuggestionCallback) { }
|
|
65
|
+
handleEvent(event: Event): boolean {
|
|
66
|
+
const { target, currentTarget } = event;
|
|
67
|
+
if (event.type !== 'click' || !(target instanceof Element)) { return false; }
|
|
68
|
+
const link = target.closest('a[href^="_callback"]');
|
|
69
|
+
if (link) {
|
|
70
|
+
this.suggestion.callback();
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
if (!(currentTarget instanceof Element)) {
|
|
74
|
+
this.suggestion.callback();
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
const containedLink = currentTarget.querySelector('a[href^="_callback"]');
|
|
78
|
+
// Whole body should count.
|
|
79
|
+
if (!containedLink) {
|
|
80
|
+
this.suggestion.callback();
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -13,31 +13,35 @@
|
|
|
13
13
|
//
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
|
-
import { ChangeSet, ChatAgent, ChatChangeEvent, ChatModel, ChatRequestModel } from '@theia/ai-chat';
|
|
16
|
+
import { ChangeSet, ChatAgent, ChatChangeEvent, ChatModel, ChatRequestModel, ChatService, ChatSuggestion } from '@theia/ai-chat';
|
|
17
17
|
import { Disposable, DisposableCollection, InMemoryResources, URI, nls } from '@theia/core';
|
|
18
|
-
import { ContextMenuRenderer, LabelProvider, Message, ReactWidget } from '@theia/core/lib/browser';
|
|
18
|
+
import { ContextMenuRenderer, LabelProvider, Message, OpenerService, ReactWidget } from '@theia/core/lib/browser';
|
|
19
19
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
20
20
|
import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify';
|
|
21
21
|
import * as React from '@theia/core/shared/react';
|
|
22
22
|
import { IMouseEvent } from '@theia/monaco-editor-core';
|
|
23
|
-
import {
|
|
23
|
+
import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
|
|
24
24
|
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
|
|
25
25
|
import { CHAT_VIEW_LANGUAGE_EXTENSION } from './chat-view-language-contribution';
|
|
26
26
|
import { AIVariableResolutionRequest } from '@theia/ai-core';
|
|
27
27
|
import { FrontendVariableService } from '@theia/ai-core/lib/browser';
|
|
28
28
|
import { ContextVariablePicker } from './context-variable-picker';
|
|
29
29
|
import { ChangeSetActionRenderer, ChangeSetActionService } from './change-set-actions/change-set-action-service';
|
|
30
|
+
import { ChangeSetDecoratorService } from '@theia/ai-chat/lib/browser/change-set-decorator-service';
|
|
31
|
+
import { ChatInputAgentSuggestions } from './chat-input-agent-suggestions';
|
|
30
32
|
|
|
31
33
|
type Query = (query: string) => Promise<void>;
|
|
32
34
|
type Unpin = () => void;
|
|
33
35
|
type Cancel = (requestModel: ChatRequestModel) => void;
|
|
34
36
|
type DeleteChangeSet = (requestModel: ChatRequestModel) => void;
|
|
35
37
|
type DeleteChangeSetElement = (requestModel: ChatRequestModel, index: number) => void;
|
|
38
|
+
type OpenContextElement = (request: AIVariableResolutionRequest) => unknown;
|
|
36
39
|
|
|
37
40
|
export const AIChatInputConfiguration = Symbol('AIChatInputConfiguration');
|
|
38
41
|
export interface AIChatInputConfiguration {
|
|
39
42
|
showContext?: boolean;
|
|
40
43
|
showPinnedAgent?: boolean;
|
|
44
|
+
showChangeSet?: boolean;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
@injectable()
|
|
@@ -69,8 +73,17 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
69
73
|
@inject(ChangeSetActionService)
|
|
70
74
|
protected readonly changeSetActionService: ChangeSetActionService;
|
|
71
75
|
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
@inject(ChangeSetDecoratorService)
|
|
77
|
+
protected readonly changeSetDecoratorService: ChangeSetDecoratorService;
|
|
78
|
+
|
|
79
|
+
@inject(OpenerService)
|
|
80
|
+
protected readonly openerService: OpenerService;
|
|
81
|
+
|
|
82
|
+
@inject(ChatService)
|
|
83
|
+
protected readonly chatService: ChatService;
|
|
84
|
+
|
|
85
|
+
protected editorRef: SimpleMonacoEditor | undefined = undefined;
|
|
86
|
+
protected readonly editorReady = new Deferred<void>();
|
|
74
87
|
|
|
75
88
|
protected isEnabled = false;
|
|
76
89
|
|
|
@@ -95,6 +108,11 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
95
108
|
this._onDeleteChangeSetElement = deleteChangeSetElement;
|
|
96
109
|
}
|
|
97
110
|
|
|
111
|
+
private _initialValue?: string;
|
|
112
|
+
set initialValue(value: string | undefined) {
|
|
113
|
+
this._initialValue = value;
|
|
114
|
+
}
|
|
115
|
+
|
|
98
116
|
protected onDisposeForChatModel = new DisposableCollection();
|
|
99
117
|
private _chatModel: ChatModel;
|
|
100
118
|
set chatModel(chatModel: ChatModel) {
|
|
@@ -130,6 +148,10 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
130
148
|
});
|
|
131
149
|
}
|
|
132
150
|
|
|
151
|
+
protected getResourceUri(): URI {
|
|
152
|
+
return new URI(`ai-chat:/input.${CHAT_VIEW_LANGUAGE_EXTENSION}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
133
155
|
protected render(): React.ReactNode {
|
|
134
156
|
return (
|
|
135
157
|
<ChatInput
|
|
@@ -142,11 +164,13 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
142
164
|
onDeleteChangeSetElement={this._onDeleteChangeSetElement.bind(this)}
|
|
143
165
|
onAddContextElement={this.addContextElement.bind(this)}
|
|
144
166
|
onDeleteContextElement={this.deleteContextElement.bind(this)}
|
|
145
|
-
context={this.
|
|
167
|
+
context={this.getContext()}
|
|
168
|
+
onOpenContextElement={this.openContextElement.bind(this)}
|
|
146
169
|
chatModel={this._chatModel}
|
|
147
170
|
pinnedAgent={this._pinnedAgent}
|
|
148
171
|
editorProvider={this.editorProvider}
|
|
149
172
|
resources={this.resources}
|
|
173
|
+
resourceUriProvider={this.getResourceUri.bind(this)}
|
|
150
174
|
contextMenuCallback={this.handleContextMenu.bind(this)}
|
|
151
175
|
isEnabled={this.isEnabled}
|
|
152
176
|
setEditorRef={editor => {
|
|
@@ -155,8 +179,13 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
155
179
|
}}
|
|
156
180
|
showContext={this.configuration?.showContext}
|
|
157
181
|
showPinnedAgent={this.configuration?.showPinnedAgent}
|
|
182
|
+
showChangeSet={this.configuration?.showChangeSet}
|
|
158
183
|
labelProvider={this.labelProvider}
|
|
159
184
|
actionService={this.changeSetActionService}
|
|
185
|
+
decoratorService={this.changeSetDecoratorService}
|
|
186
|
+
initialValue={this._initialValue}
|
|
187
|
+
openerService={this.openerService}
|
|
188
|
+
suggestions={this._chatModel.suggestions}
|
|
160
189
|
/>
|
|
161
190
|
);
|
|
162
191
|
}
|
|
@@ -195,6 +224,12 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
195
224
|
});
|
|
196
225
|
}
|
|
197
226
|
|
|
227
|
+
protected async openContextElement(request: AIVariableResolutionRequest): Promise<void> {
|
|
228
|
+
const session = this.chatService.getSessions().find(candidate => candidate.model.id === this._chatModel.id);
|
|
229
|
+
const context = { session };
|
|
230
|
+
await this.variableService.open(request, context);
|
|
231
|
+
}
|
|
232
|
+
|
|
198
233
|
public setEnabled(enabled: boolean): void {
|
|
199
234
|
this.isEnabled = enabled;
|
|
200
235
|
this.update();
|
|
@@ -203,7 +238,7 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
203
238
|
protected addContextElement(): void {
|
|
204
239
|
this.contextVariablePicker.pickContextVariable().then(contextElement => {
|
|
205
240
|
if (contextElement) {
|
|
206
|
-
this.
|
|
241
|
+
this.addContext(contextElement);
|
|
207
242
|
}
|
|
208
243
|
});
|
|
209
244
|
}
|
|
@@ -225,6 +260,10 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
225
260
|
addContext(variable: AIVariableResolutionRequest): void {
|
|
226
261
|
this._chatModel.context.addVariables(variable);
|
|
227
262
|
}
|
|
263
|
+
|
|
264
|
+
protected getContext(): readonly AIVariableResolutionRequest[] {
|
|
265
|
+
return this._chatModel.context.getVariables();
|
|
266
|
+
}
|
|
228
267
|
}
|
|
229
268
|
|
|
230
269
|
interface ChatInputProperties {
|
|
@@ -237,18 +276,25 @@ interface ChatInputProperties {
|
|
|
237
276
|
onDeleteChangeSetElement: (sessionId: string, index: number) => void;
|
|
238
277
|
onAddContextElement: () => void;
|
|
239
278
|
onDeleteContextElement: (index: number) => void;
|
|
279
|
+
onOpenContextElement: OpenContextElement;
|
|
240
280
|
context?: readonly AIVariableResolutionRequest[];
|
|
241
281
|
isEnabled?: boolean;
|
|
242
282
|
chatModel: ChatModel;
|
|
243
283
|
pinnedAgent?: ChatAgent;
|
|
244
284
|
editorProvider: MonacoEditorProvider;
|
|
245
285
|
resources: InMemoryResources;
|
|
286
|
+
resourceUriProvider: () => URI;
|
|
246
287
|
contextMenuCallback: (event: IMouseEvent) => void;
|
|
247
|
-
setEditorRef: (editor:
|
|
288
|
+
setEditorRef: (editor: SimpleMonacoEditor | undefined) => void;
|
|
248
289
|
showContext?: boolean;
|
|
249
290
|
showPinnedAgent?: boolean;
|
|
291
|
+
showChangeSet?: boolean;
|
|
250
292
|
labelProvider: LabelProvider;
|
|
251
293
|
actionService: ChangeSetActionService;
|
|
294
|
+
decoratorService: ChangeSetDecoratorService;
|
|
295
|
+
initialValue?: string;
|
|
296
|
+
openerService: OpenerService;
|
|
297
|
+
suggestions: readonly ChatSuggestion[]
|
|
252
298
|
}
|
|
253
299
|
|
|
254
300
|
const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInputProperties) => {
|
|
@@ -262,6 +308,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
262
308
|
? buildChangeSetUI(
|
|
263
309
|
props.chatModel.changeSet,
|
|
264
310
|
props.labelProvider,
|
|
311
|
+
props.decoratorService,
|
|
265
312
|
props.actionService.getActionsForChangeset(props.chatModel.changeSet),
|
|
266
313
|
onDeleteChangeSet,
|
|
267
314
|
onDeleteChangeSetElement
|
|
@@ -273,16 +320,16 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
273
320
|
const editorContainerRef = React.useRef<HTMLDivElement | null>(null);
|
|
274
321
|
// eslint-disable-next-line no-null/no-null
|
|
275
322
|
const placeholderRef = React.useRef<HTMLDivElement | null>(null);
|
|
276
|
-
const editorRef = React.useRef<
|
|
323
|
+
const editorRef = React.useRef<SimpleMonacoEditor | undefined>(undefined);
|
|
277
324
|
|
|
278
325
|
React.useEffect(() => {
|
|
279
|
-
const uri =
|
|
326
|
+
const uri = props.resourceUriProvider();
|
|
280
327
|
const resource = props.resources.add(uri, '');
|
|
281
328
|
const createInputElement = async () => {
|
|
282
329
|
const paddingTop = 6;
|
|
283
330
|
const lineHeight = 20;
|
|
284
331
|
const maxHeight = 240;
|
|
285
|
-
const editor = await props.editorProvider.
|
|
332
|
+
const editor = await props.editorProvider.createSimpleInline(uri, editorContainerRef.current!, {
|
|
286
333
|
language: CHAT_VIEW_LANGUAGE_EXTENSION,
|
|
287
334
|
// Disable code lens, inlay hints and hover support to avoid console errors from other contributions
|
|
288
335
|
codeLens: false,
|
|
@@ -323,6 +370,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
323
370
|
editorContainerRef.current.style.height = `${Math.min(contentHeight, maxHeight)}px`;
|
|
324
371
|
}
|
|
325
372
|
};
|
|
373
|
+
|
|
326
374
|
editor.getControl().onDidChangeModelContent(() => {
|
|
327
375
|
const value = editor.getControl().getValue();
|
|
328
376
|
setIsInputEmpty(!value || value.length === 0);
|
|
@@ -343,6 +391,10 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
343
391
|
|
|
344
392
|
editorRef.current = editor;
|
|
345
393
|
props.setEditorRef(editor);
|
|
394
|
+
|
|
395
|
+
if (props.initialValue) {
|
|
396
|
+
setValue(props.initialValue);
|
|
397
|
+
}
|
|
346
398
|
};
|
|
347
399
|
createInputElement();
|
|
348
400
|
|
|
@@ -376,6 +428,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
376
428
|
setChangeSetUI(buildChangeSetUI(
|
|
377
429
|
event.changeSet,
|
|
378
430
|
props.labelProvider,
|
|
431
|
+
props.decoratorService,
|
|
379
432
|
props.actionService.getActionsForChangeset(event.changeSet),
|
|
380
433
|
onDeleteChangeSet,
|
|
381
434
|
onDeleteChangeSetElement
|
|
@@ -387,6 +440,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
387
440
|
? buildChangeSetUI(
|
|
388
441
|
props.chatModel.changeSet,
|
|
389
442
|
props.labelProvider,
|
|
443
|
+
props.decoratorService,
|
|
390
444
|
props.actionService.getActionsForChangeset(props.chatModel.changeSet),
|
|
391
445
|
onDeleteChangeSet,
|
|
392
446
|
onDeleteChangeSetElement
|
|
@@ -408,16 +462,37 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
408
462
|
return () => disposable.dispose();
|
|
409
463
|
});
|
|
410
464
|
|
|
465
|
+
React.useEffect(() => {
|
|
466
|
+
const disposable = props.decoratorService.onDidChangeDecorations(() => {
|
|
467
|
+
if (!props.chatModel.changeSet) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
setChangeSetUI(buildChangeSetUI(
|
|
471
|
+
props.chatModel.changeSet,
|
|
472
|
+
props.labelProvider,
|
|
473
|
+
props.decoratorService,
|
|
474
|
+
props.actionService.getActionsForChangeset(props.chatModel.changeSet),
|
|
475
|
+
onDeleteChangeSet,
|
|
476
|
+
onDeleteChangeSetElement
|
|
477
|
+
));
|
|
478
|
+
});
|
|
479
|
+
return () => disposable.dispose();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const setValue = React.useCallback((value: string) => {
|
|
483
|
+
if (editorRef.current && !editorRef.current.document.isDisposed()) {
|
|
484
|
+
editorRef.current.document.textEditorModel.setValue(value);
|
|
485
|
+
}
|
|
486
|
+
}, [editorRef]);
|
|
487
|
+
|
|
411
488
|
const submit = React.useCallback(function submit(value: string): void {
|
|
412
489
|
if (!value || value.trim().length === 0) {
|
|
413
490
|
return;
|
|
414
491
|
}
|
|
415
492
|
setInProgress(true);
|
|
416
493
|
props.onQuery(value);
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
}, [props.context, props.onQuery, editorRef]);
|
|
494
|
+
setValue('');
|
|
495
|
+
}, [props.context, props.onQuery, setValue]);
|
|
421
496
|
|
|
422
497
|
const onKeyDown = React.useCallback((event: React.KeyboardEvent) => {
|
|
423
498
|
if (!props.isEnabled) {
|
|
@@ -515,10 +590,11 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
515
590
|
disabled: isInputEmpty || !props.isEnabled
|
|
516
591
|
}];
|
|
517
592
|
|
|
518
|
-
const contextUI = buildContextUI(props.context, props.labelProvider, props.onDeleteContextElement);
|
|
593
|
+
const contextUI = buildContextUI(props.context, props.labelProvider, props.onDeleteContextElement, props.onOpenContextElement);
|
|
519
594
|
|
|
520
595
|
return <div className='theia-ChatInput' onDragOver={props.onDragOver} onDrop={props.onDrop} >
|
|
521
|
-
{
|
|
596
|
+
{<ChatInputAgentSuggestions suggestions={props.suggestions} opener={props.openerService} />}
|
|
597
|
+
{props.showChangeSet && changeSetUI?.elements &&
|
|
522
598
|
<ChangeSetBox changeSet={changeSetUI} />
|
|
523
599
|
}
|
|
524
600
|
<div className='theia-ChatInput-Editor-Box'>
|
|
@@ -541,6 +617,7 @@ const noPropagation = (handler: () => void) => (e: React.MouseEvent) => {
|
|
|
541
617
|
const buildChangeSetUI = (
|
|
542
618
|
changeSet: ChangeSet,
|
|
543
619
|
labelProvider: LabelProvider,
|
|
620
|
+
decoratorService: ChangeSetDecoratorService,
|
|
544
621
|
actions: ChangeSetActionRenderer[],
|
|
545
622
|
onDeleteChangeSet: () => void,
|
|
546
623
|
onDeleteChangeSetElement: (index: number) => void
|
|
@@ -554,11 +631,12 @@ const buildChangeSetUI = (
|
|
|
554
631
|
nameClass: `${element.type} ${element.state}`,
|
|
555
632
|
name: element.name ?? labelProvider.getName(element.uri),
|
|
556
633
|
additionalInfo: element.additionalInfo ?? labelProvider.getDetails(element.uri),
|
|
634
|
+
additionalInfoSuffixIcon: decoratorService.getAdditionalInfoSuffixIcon(element),
|
|
557
635
|
openChange: element?.openChange?.bind(element),
|
|
558
636
|
apply: element.state !== 'applied' ? element?.apply?.bind(element) : undefined,
|
|
559
637
|
revert: element.state === 'applied' || element.state === 'stale' ? element?.revert?.bind(element) : undefined,
|
|
560
638
|
delete: () => onDeleteChangeSetElement(changeSet.getElements().indexOf(element))
|
|
561
|
-
})),
|
|
639
|
+
} satisfies ChangeSetUIElement)),
|
|
562
640
|
actions
|
|
563
641
|
});
|
|
564
642
|
|
|
@@ -567,6 +645,7 @@ interface ChangeSetUIElement {
|
|
|
567
645
|
iconClass: string;
|
|
568
646
|
nameClass: string;
|
|
569
647
|
additionalInfo: string;
|
|
648
|
+
additionalInfoSuffixIcon?: string[];
|
|
570
649
|
open?: () => void;
|
|
571
650
|
openChange?: () => void;
|
|
572
651
|
apply?: () => void;
|
|
@@ -596,15 +675,18 @@ const ChangeSetBox: React.FunctionComponent<{ changeSet: ChangeSetUI }> = React.
|
|
|
596
675
|
<ul>
|
|
597
676
|
{elements.map((element, index) => (
|
|
598
677
|
<li key={index} title={nls.localize('theia/ai/chat-ui/openDiff', 'Open Diff')} onClick={() => element.openChange?.()}>
|
|
599
|
-
<div className={`theia-ChatInput-ChangeSet-Icon ${element.iconClass}`}
|
|
600
|
-
|
|
678
|
+
<div className={`theia-ChatInput-ChangeSet-Icon ${element.iconClass}`}>
|
|
679
|
+
</div>
|
|
680
|
+
<div className='theia-ChatInput-ChangeSet-labelParts'>
|
|
601
681
|
<span className={`theia-ChatInput-ChangeSet-title ${element.nameClass}`}>
|
|
602
682
|
{element.name}
|
|
603
683
|
</span>
|
|
604
|
-
<
|
|
605
|
-
{element.additionalInfo}
|
|
606
|
-
|
|
607
|
-
|
|
684
|
+
<div className='theia-ChatInput-ChangeSet-additionalInfo'>
|
|
685
|
+
{element.additionalInfo && <span>{element.additionalInfo}</span>}
|
|
686
|
+
{element.additionalInfoSuffixIcon
|
|
687
|
+
&& <div className={`theia-ChatInput-ChangeSet-AdditionalInfo-SuffixIcon ${element.additionalInfoSuffixIcon.join(' ')}`}></div>}
|
|
688
|
+
</div>
|
|
689
|
+
</div>
|
|
608
690
|
<div className='theia-ChatInput-ChangeSet-Actions'>
|
|
609
691
|
{element.open && (
|
|
610
692
|
<span
|
|
@@ -685,7 +767,12 @@ function getLatestRequest(chatModel: ChatModel): ChatRequestModel | undefined {
|
|
|
685
767
|
return requests.length > 0 ? requests[requests.length - 1] : undefined;
|
|
686
768
|
}
|
|
687
769
|
|
|
688
|
-
function buildContextUI(
|
|
770
|
+
function buildContextUI(
|
|
771
|
+
context: readonly AIVariableResolutionRequest[] | undefined,
|
|
772
|
+
labelProvider: LabelProvider,
|
|
773
|
+
onDeleteContextElement: (index: number) => void,
|
|
774
|
+
onOpen: OpenContextElement
|
|
775
|
+
): ChatContextUI {
|
|
689
776
|
if (!context) {
|
|
690
777
|
return { context: [] };
|
|
691
778
|
}
|
|
@@ -697,6 +784,7 @@ function buildContextUI(context: readonly AIVariableResolutionRequest[] | undefi
|
|
|
697
784
|
additionalInfo: labelProvider.getDetails(element),
|
|
698
785
|
details: labelProvider.getLongName(element),
|
|
699
786
|
delete: () => onDeleteContextElement(index),
|
|
787
|
+
open: () => onOpen(element)
|
|
700
788
|
}))
|
|
701
789
|
};
|
|
702
790
|
}
|
|
@@ -719,13 +807,15 @@ const ChatContext: React.FunctionComponent<ChatContextUI> = ({ context }) => (
|
|
|
719
807
|
{context.map((element, index) => (
|
|
720
808
|
<li key={index} className="theia-ChatInput-ChatContext-Element" title={element.details} onClick={() => element.open?.()}>
|
|
721
809
|
<div className={`theia-ChatInput-ChatContext-Icon ${element.iconClass}`} />
|
|
722
|
-
<
|
|
723
|
-
{element.
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
810
|
+
<div className="theia-ChatInput-ChatContext-labelParts">
|
|
811
|
+
<span className={`theia-ChatInput-ChatContext-title ${element.nameClass}`}>
|
|
812
|
+
{element.name}
|
|
813
|
+
</span>
|
|
814
|
+
<span className='theia-ChatInput-ChatContext-additionalInfo'>
|
|
815
|
+
{element.additionalInfo}
|
|
816
|
+
</span>
|
|
817
|
+
</div>
|
|
818
|
+
<span className="codicon codicon-close action" title={nls.localizeByDefault('Delete')} onClick={e => { e.stopPropagation(); element.delete(); }} />
|
|
729
819
|
</li>
|
|
730
820
|
))}
|
|
731
821
|
</ul>
|
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
//
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
|
-
import {
|
|
16
|
+
import { Command, nls } from '@theia/core';
|
|
17
|
+
import { codicon } from '@theia/core/lib/browser';
|
|
18
|
+
import { isRequestNode, RequestNode, ResponseNode } from './chat-tree-view';
|
|
19
|
+
import { EditableChatRequestModel } from '@theia/ai-chat';
|
|
17
20
|
|
|
18
21
|
export interface ChatNodeToolbarAction {
|
|
19
22
|
/**
|
|
@@ -61,3 +64,39 @@ export interface ChatNodeToolbarActionContribution {
|
|
|
61
64
|
*/
|
|
62
65
|
getToolbarActions(node: RequestNode | ResponseNode): ChatNodeToolbarAction[];
|
|
63
66
|
}
|
|
67
|
+
|
|
68
|
+
export namespace ChatNodeToolbarCommands {
|
|
69
|
+
const CHAT_NODE_TOOLBAR_CATEGORY = 'ChatNodeToolbar';
|
|
70
|
+
const CHAT_NODE_TOOLBAR_CATEGORY_KEY = nls.getDefaultKey(CHAT_NODE_TOOLBAR_CATEGORY);
|
|
71
|
+
|
|
72
|
+
export const EDIT = Command.toLocalizedCommand({
|
|
73
|
+
id: 'chat:node:toolbar:edit-request',
|
|
74
|
+
category: CHAT_NODE_TOOLBAR_CATEGORY,
|
|
75
|
+
}, '', CHAT_NODE_TOOLBAR_CATEGORY_KEY);
|
|
76
|
+
|
|
77
|
+
export const CANCEL = Command.toLocalizedCommand({
|
|
78
|
+
id: 'chat:node:toolbar:cancel-request',
|
|
79
|
+
category: CHAT_NODE_TOOLBAR_CATEGORY,
|
|
80
|
+
}, '', CHAT_NODE_TOOLBAR_CATEGORY_KEY);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export class DefaultChatNodeToolbarActionContribution implements ChatNodeToolbarActionContribution {
|
|
84
|
+
getToolbarActions(node: RequestNode | ResponseNode): ChatNodeToolbarAction[] {
|
|
85
|
+
if (isRequestNode(node)) {
|
|
86
|
+
if (EditableChatRequestModel.isEditing(node.request)) {
|
|
87
|
+
return [{
|
|
88
|
+
commandId: ChatNodeToolbarCommands.CANCEL.id,
|
|
89
|
+
icon: codicon('close'),
|
|
90
|
+
tooltip: nls.localize('theia/ai/chat-ui/node/toolbar/cancel', 'Cancel'),
|
|
91
|
+
}];
|
|
92
|
+
}
|
|
93
|
+
return [{
|
|
94
|
+
commandId: ChatNodeToolbarCommands.EDIT.id,
|
|
95
|
+
icon: codicon('edit'),
|
|
96
|
+
tooltip: nls.localize('theia/ai/chat-ui/node/toolbar/edit', 'Edit'),
|
|
97
|
+
}];
|
|
98
|
+
} else {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
|
|
17
|
+
import { ChatProgressMessage } from '@theia/ai-chat';
|
|
18
|
+
import * as React from '@theia/core/shared/react';
|
|
19
|
+
|
|
20
|
+
export type ProgressMessageProps = Omit<ChatProgressMessage, 'kind' | 'id' | 'show'>;
|
|
21
|
+
|
|
22
|
+
export const ProgressMessage = (c: ProgressMessageProps) => (
|
|
23
|
+
<div className='theia-ResponseNode-ProgressMessage'>
|
|
24
|
+
<Indicator {...c} /> {c.content}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export const Indicator = (progressMessage: ProgressMessageProps) => (
|
|
29
|
+
<span className='theia-ResponseNode-ProgressMessage-Indicator'>
|
|
30
|
+
{progressMessage.status === 'inProgress' &&
|
|
31
|
+
<i className={'fa fa-spinner fa-spin ' + progressMessage.status}></i>
|
|
32
|
+
}
|
|
33
|
+
{progressMessage.status === 'completed' &&
|
|
34
|
+
<i className={'fa fa-check ' + progressMessage.status}></i>
|
|
35
|
+
}
|
|
36
|
+
{progressMessage.status === 'failed' &&
|
|
37
|
+
<i className={'fa fa-warning ' + progressMessage.status}></i>
|
|
38
|
+
}
|
|
39
|
+
</span>
|
|
40
|
+
);
|
|
@@ -26,7 +26,7 @@ import { ReactNode } from '@theia/core/shared/react';
|
|
|
26
26
|
import { nls } from '@theia/core/lib/common/nls';
|
|
27
27
|
import { Position } from '@theia/core/shared/vscode-languageserver-protocol';
|
|
28
28
|
import { EditorManager, EditorWidget } from '@theia/editor/lib/browser';
|
|
29
|
-
import {
|
|
29
|
+
import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
|
|
30
30
|
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
|
|
31
31
|
import { MonacoLanguages } from '@theia/monaco/lib/browser/monaco-languages';
|
|
32
32
|
import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
|
|
@@ -206,11 +206,11 @@ export const CodeWrapper = (props: {
|
|
|
206
206
|
}) => {
|
|
207
207
|
// eslint-disable-next-line no-null/no-null
|
|
208
208
|
const ref = React.useRef<HTMLDivElement | null>(null);
|
|
209
|
-
const editorRef = React.useRef<
|
|
209
|
+
const editorRef = React.useRef<SimpleMonacoEditor | undefined>(undefined);
|
|
210
210
|
|
|
211
211
|
const createInputElement = async () => {
|
|
212
212
|
const resource = await props.untitledResourceResolver.createUntitledResource(undefined, props.language);
|
|
213
|
-
const editor = await props.editorProvider.
|
|
213
|
+
const editor = await props.editorProvider.createSimpleInline(resource.uri, ref.current!, {
|
|
214
214
|
readOnly: true,
|
|
215
215
|
autoSizing: true,
|
|
216
216
|
scrollBeyondLastLine: false,
|
|
@@ -60,6 +60,10 @@ const MarkdownRender = ({ response, openerService }: { response: MarkdownChatRes
|
|
|
60
60
|
return <div ref={ref}></div>;
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
+
export interface DeclaredEventsEventListenerObject extends EventListenerObject {
|
|
64
|
+
handledEvents?: (keyof HTMLElementEventMap)[];
|
|
65
|
+
}
|
|
66
|
+
|
|
63
67
|
/**
|
|
64
68
|
* This hook uses markdown-it directly to render markdown.
|
|
65
69
|
* The reason to use markdown-it directly is that the MarkdownRenderer is
|
|
@@ -72,9 +76,17 @@ const MarkdownRender = ({ response, openerService }: { response: MarkdownChatRes
|
|
|
72
76
|
* @param markdown the string to render as markdown
|
|
73
77
|
* @param skipSurroundingParagraph whether to remove a surrounding paragraph element (default: false)
|
|
74
78
|
* @param openerService the service to handle link opening
|
|
79
|
+
* @param eventHandler `handleEvent` will be called by default for `click` events and additionally
|
|
80
|
+
* for all events enumerated in {@link DeclaredEventsEventListenerObject.handledEvents}. If `handleEvent` returns `true`,
|
|
81
|
+
* no additional handlers will be run for the event.
|
|
75
82
|
* @returns the ref to use in an element to render the markdown
|
|
76
83
|
*/
|
|
77
|
-
export const useMarkdownRendering = (
|
|
84
|
+
export const useMarkdownRendering = (
|
|
85
|
+
markdown: string | MarkdownString,
|
|
86
|
+
openerService: OpenerService,
|
|
87
|
+
skipSurroundingParagraph: boolean = false,
|
|
88
|
+
eventHandler?: DeclaredEventsEventListenerObject
|
|
89
|
+
) => {
|
|
78
90
|
// null is valid in React
|
|
79
91
|
// eslint-disable-next-line no-null/no-null
|
|
80
92
|
const ref = useRef<HTMLDivElement | null>(null);
|
|
@@ -98,6 +110,7 @@ export const useMarkdownRendering = (markdown: string | MarkdownString, openerSe
|
|
|
98
110
|
|
|
99
111
|
// intercept link clicks to use the Theia OpenerService instead of the default browser behavior
|
|
100
112
|
const handleClick = (event: MouseEvent) => {
|
|
113
|
+
if ((eventHandler?.handleEvent(event) as unknown) === true) {return; }
|
|
101
114
|
let target = event.target as HTMLElement;
|
|
102
115
|
while (target && target.tagName !== 'A') {
|
|
103
116
|
target = target.parentElement as HTMLElement;
|
|
@@ -112,7 +125,11 @@ export const useMarkdownRendering = (markdown: string | MarkdownString, openerSe
|
|
|
112
125
|
};
|
|
113
126
|
|
|
114
127
|
ref?.current?.addEventListener('click', handleClick);
|
|
115
|
-
|
|
128
|
+
eventHandler?.handledEvents?.forEach(eventType => eventType !== 'click' && ref?.current?.addEventListener(eventType, eventHandler));
|
|
129
|
+
return () => {
|
|
130
|
+
ref.current?.removeEventListener('click', handleClick);
|
|
131
|
+
eventHandler?.handledEvents?.forEach(eventType => eventType !== 'click' && ref?.current?.removeEventListener(eventType, eventHandler));
|
|
132
|
+
};
|
|
116
133
|
}, [markdownString, skipSurroundingParagraph, openerService]);
|
|
117
134
|
|
|
118
135
|
return ref;
|