@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
|
@@ -13,27 +13,32 @@
|
|
|
13
13
|
//
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
|
-
import {
|
|
17
|
-
import { UntitledResourceResolver } from '@theia/core';
|
|
18
|
-
import { ContextMenuRenderer, Message, ReactWidget } from '@theia/core/lib/browser';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
16
|
+
import { ChangeSet, ChangeSetElement, ChatChangeEvent, ChatModel, ChatRequestModel } from '@theia/ai-chat';
|
|
17
|
+
import { Disposable, UntitledResourceResolver } from '@theia/core';
|
|
18
|
+
import { ContextMenuRenderer, LabelProvider, Message, ReactWidget } from '@theia/core/lib/browser';
|
|
19
|
+
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
20
|
+
import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify';
|
|
21
21
|
import * as React from '@theia/core/shared/react';
|
|
22
|
+
import { IMouseEvent } from '@theia/monaco-editor-core';
|
|
23
|
+
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
|
22
24
|
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
|
|
23
25
|
import { CHAT_VIEW_LANGUAGE_EXTENSION } from './chat-view-language-contribution';
|
|
24
|
-
import { IMouseEvent } from '@theia/monaco-editor-core';
|
|
25
26
|
|
|
26
27
|
type Query = (query: string) => Promise<void>;
|
|
27
28
|
type Cancel = (requestModel: ChatRequestModel) => void;
|
|
29
|
+
type DeleteChangeSet = (requestModel: ChatRequestModel) => void;
|
|
30
|
+
type DeleteChangeSetElement = (requestModel: ChatRequestModel, index: number) => void;
|
|
31
|
+
|
|
32
|
+
export const AIChatInputConfiguration = Symbol('AIChatInputConfiguration');
|
|
33
|
+
export interface AIChatInputConfiguration {
|
|
34
|
+
showContext?: boolean;
|
|
35
|
+
}
|
|
28
36
|
|
|
29
37
|
@injectable()
|
|
30
38
|
export class AIChatInputWidget extends ReactWidget {
|
|
31
39
|
public static ID = 'chat-input-widget';
|
|
32
40
|
static readonly CONTEXT_MENU = ['chat-input-context-menu'];
|
|
33
41
|
|
|
34
|
-
@inject(ChatAgentService)
|
|
35
|
-
protected readonly agentService: ChatAgentService;
|
|
36
|
-
|
|
37
42
|
@inject(MonacoEditorProvider)
|
|
38
43
|
protected readonly editorProvider: MonacoEditorProvider;
|
|
39
44
|
|
|
@@ -43,6 +48,15 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
43
48
|
@inject(ContextMenuRenderer)
|
|
44
49
|
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
|
45
50
|
|
|
51
|
+
@inject(AIChatInputConfiguration) @optional()
|
|
52
|
+
protected readonly configuration: AIChatInputConfiguration | undefined;
|
|
53
|
+
|
|
54
|
+
@inject(LabelProvider)
|
|
55
|
+
protected readonly labelProvider: LabelProvider;
|
|
56
|
+
|
|
57
|
+
protected editorRef: MonacoEditor | undefined = undefined;
|
|
58
|
+
private editorReady = new Deferred<void>();
|
|
59
|
+
|
|
46
60
|
protected isEnabled = false;
|
|
47
61
|
|
|
48
62
|
private _onQuery: Query;
|
|
@@ -53,6 +67,14 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
53
67
|
set onCancel(cancel: Cancel) {
|
|
54
68
|
this._onCancel = cancel;
|
|
55
69
|
}
|
|
70
|
+
private _onDeleteChangeSet: DeleteChangeSet;
|
|
71
|
+
set onDeleteChangeSet(deleteChangeSet: DeleteChangeSet) {
|
|
72
|
+
this._onDeleteChangeSet = deleteChangeSet;
|
|
73
|
+
}
|
|
74
|
+
private _onDeleteChangeSetElement: DeleteChangeSetElement;
|
|
75
|
+
set onDeleteChangeSetElement(deleteChangeSetElement: DeleteChangeSetElement) {
|
|
76
|
+
this._onDeleteChangeSetElement = deleteChangeSetElement;
|
|
77
|
+
}
|
|
56
78
|
private _chatModel: ChatModel;
|
|
57
79
|
set chatModel(chatModel: ChatModel) {
|
|
58
80
|
this._chatModel = chatModel;
|
|
@@ -65,13 +87,14 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
65
87
|
this.title.closable = false;
|
|
66
88
|
this.update();
|
|
67
89
|
}
|
|
90
|
+
|
|
68
91
|
protected override onActivateRequest(msg: Message): void {
|
|
69
92
|
super.onActivateRequest(msg);
|
|
70
|
-
this.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
93
|
+
this.editorReady.promise.then(() => {
|
|
94
|
+
if (this.editorRef) {
|
|
95
|
+
this.editorRef.focus();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
75
98
|
}
|
|
76
99
|
|
|
77
100
|
protected render(): React.ReactNode {
|
|
@@ -79,12 +102,19 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
79
102
|
<ChatInput
|
|
80
103
|
onQuery={this._onQuery.bind(this)}
|
|
81
104
|
onCancel={this._onCancel.bind(this)}
|
|
105
|
+
onDeleteChangeSet={this._onDeleteChangeSet.bind(this)}
|
|
106
|
+
onDeleteChangeSetElement={this._onDeleteChangeSetElement.bind(this)}
|
|
82
107
|
chatModel={this._chatModel}
|
|
83
|
-
getChatAgents={this.getChatAgents.bind(this)}
|
|
84
108
|
editorProvider={this.editorProvider}
|
|
85
109
|
untitledResourceResolver={this.untitledResourceResolver}
|
|
86
110
|
contextMenuCallback={this.handleContextMenu.bind(this)}
|
|
87
111
|
isEnabled={this.isEnabled}
|
|
112
|
+
setEditorRef={editor => {
|
|
113
|
+
this.editorRef = editor;
|
|
114
|
+
this.editorReady.resolve();
|
|
115
|
+
}}
|
|
116
|
+
showContext={this.configuration?.showContext}
|
|
117
|
+
labelProvider={this.labelProvider}
|
|
88
118
|
/>
|
|
89
119
|
);
|
|
90
120
|
}
|
|
@@ -107,105 +137,150 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
107
137
|
interface ChatInputProperties {
|
|
108
138
|
onCancel: (requestModel: ChatRequestModel) => void;
|
|
109
139
|
onQuery: (query: string) => void;
|
|
140
|
+
onDeleteChangeSet: (sessionId: string) => void;
|
|
141
|
+
onDeleteChangeSetElement: (sessionId: string, index: number) => void;
|
|
110
142
|
isEnabled?: boolean;
|
|
111
143
|
chatModel: ChatModel;
|
|
112
|
-
getChatAgents: () => ChatAgent[];
|
|
113
144
|
editorProvider: MonacoEditorProvider;
|
|
114
145
|
untitledResourceResolver: UntitledResourceResolver;
|
|
115
146
|
contextMenuCallback: (event: IMouseEvent) => void;
|
|
147
|
+
setEditorRef: (editor: MonacoEditor | undefined) => void;
|
|
148
|
+
showContext?: boolean;
|
|
149
|
+
labelProvider: LabelProvider;
|
|
116
150
|
}
|
|
151
|
+
|
|
117
152
|
const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInputProperties) => {
|
|
153
|
+
const onDeleteChangeSet = () => props.onDeleteChangeSet(props.chatModel.id);
|
|
154
|
+
const onDeleteChangeSetElement = (index: number) => props.onDeleteChangeSetElement(props.chatModel.id, index);
|
|
118
155
|
|
|
119
156
|
const [inProgress, setInProgress] = React.useState(false);
|
|
157
|
+
const [isInputEmpty, setIsInputEmpty] = React.useState(true);
|
|
158
|
+
const [changeSetUI, setChangeSetUI] = React.useState(
|
|
159
|
+
() => props.chatModel.changeSet ? buildChangeSetUI(props.chatModel.changeSet, props.labelProvider, onDeleteChangeSet, onDeleteChangeSetElement) : undefined
|
|
160
|
+
);
|
|
161
|
+
|
|
120
162
|
// eslint-disable-next-line no-null/no-null
|
|
121
163
|
const editorContainerRef = React.useRef<HTMLDivElement | null>(null);
|
|
122
164
|
// eslint-disable-next-line no-null/no-null
|
|
123
165
|
const placeholderRef = React.useRef<HTMLDivElement | null>(null);
|
|
124
166
|
const editorRef = React.useRef<MonacoEditor | undefined>(undefined);
|
|
125
|
-
const allRequests = props.chatModel.getRequests();
|
|
126
|
-
const lastRequest = allRequests.length === 0 ? undefined : allRequests[allRequests.length - 1];
|
|
127
|
-
|
|
128
|
-
const createInputElement = async () => {
|
|
129
|
-
const resource = await props.untitledResourceResolver.createUntitledResource('', CHAT_VIEW_LANGUAGE_EXTENSION);
|
|
130
|
-
const editor = await props.editorProvider.createInline(resource.uri, editorContainerRef.current!, {
|
|
131
|
-
language: CHAT_VIEW_LANGUAGE_EXTENSION,
|
|
132
|
-
// Disable code lens, inlay hints and hover support to avoid console errors from other contributions
|
|
133
|
-
codeLens: false,
|
|
134
|
-
inlayHints: { enabled: 'off' },
|
|
135
|
-
hover: { enabled: false },
|
|
136
|
-
autoSizing: true,
|
|
137
|
-
scrollBeyondLastLine: false,
|
|
138
|
-
scrollBeyondLastColumn: 0,
|
|
139
|
-
minHeight: 1,
|
|
140
|
-
fontFamily: 'var(--theia-ui-font-family)',
|
|
141
|
-
fontSize: 13,
|
|
142
|
-
cursorWidth: 1,
|
|
143
|
-
maxHeight: -1,
|
|
144
|
-
scrollbar: { horizontal: 'hidden' },
|
|
145
|
-
automaticLayout: true,
|
|
146
|
-
lineNumbers: 'off',
|
|
147
|
-
lineHeight: 20,
|
|
148
|
-
padding: { top: 8 },
|
|
149
|
-
suggest: {
|
|
150
|
-
showIcons: true,
|
|
151
|
-
showSnippets: false,
|
|
152
|
-
showWords: false,
|
|
153
|
-
showStatusBar: false,
|
|
154
|
-
insertMode: 'replace',
|
|
155
|
-
},
|
|
156
|
-
bracketPairColorization: { enabled: false },
|
|
157
|
-
wrappingStrategy: 'advanced',
|
|
158
|
-
stickyScroll: { enabled: false },
|
|
159
|
-
});
|
|
160
167
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
168
|
+
React.useEffect(() => {
|
|
169
|
+
const createInputElement = async () => {
|
|
170
|
+
const paddingTop = 6;
|
|
171
|
+
const lineHeight = 20;
|
|
172
|
+
const maxHeight = 240;
|
|
173
|
+
const resource = await props.untitledResourceResolver.createUntitledResource('', CHAT_VIEW_LANGUAGE_EXTENSION);
|
|
174
|
+
const editor = await props.editorProvider.createInline(resource.uri, editorContainerRef.current!, {
|
|
175
|
+
language: CHAT_VIEW_LANGUAGE_EXTENSION,
|
|
176
|
+
// Disable code lens, inlay hints and hover support to avoid console errors from other contributions
|
|
177
|
+
codeLens: false,
|
|
178
|
+
inlayHints: { enabled: 'off' },
|
|
179
|
+
hover: { enabled: false },
|
|
180
|
+
autoSizing: false, // we handle the sizing ourselves
|
|
181
|
+
scrollBeyondLastLine: false,
|
|
182
|
+
scrollBeyondLastColumn: 0,
|
|
183
|
+
minHeight: 1,
|
|
184
|
+
fontFamily: 'var(--theia-ui-font-family)',
|
|
185
|
+
fontSize: 13,
|
|
186
|
+
cursorWidth: 1,
|
|
187
|
+
maxHeight: -1,
|
|
188
|
+
scrollbar: { horizontal: 'hidden' },
|
|
189
|
+
automaticLayout: true,
|
|
190
|
+
lineNumbers: 'off',
|
|
191
|
+
lineHeight,
|
|
192
|
+
padding: { top: paddingTop },
|
|
193
|
+
suggest: {
|
|
194
|
+
showIcons: true,
|
|
195
|
+
showSnippets: false,
|
|
196
|
+
showWords: false,
|
|
197
|
+
showStatusBar: false,
|
|
198
|
+
insertMode: 'replace',
|
|
199
|
+
},
|
|
200
|
+
bracketPairColorization: { enabled: false },
|
|
201
|
+
wrappingStrategy: 'advanced',
|
|
202
|
+
stickyScroll: { enabled: false },
|
|
203
|
+
});
|
|
164
204
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
205
|
+
if (editorContainerRef.current) {
|
|
206
|
+
editorContainerRef.current.style.height = (lineHeight + (2 * paddingTop)) + 'px';
|
|
207
|
+
}
|
|
168
208
|
|
|
169
|
-
|
|
170
|
-
|
|
209
|
+
const updateEditorHeight = () => {
|
|
210
|
+
if (editorContainerRef.current) {
|
|
211
|
+
const contentHeight = editor.getControl().getContentHeight() + paddingTop;
|
|
212
|
+
editorContainerRef.current.style.height = `${Math.min(contentHeight, maxHeight)}px`;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
editor.getControl().onDidChangeModelContent(() => {
|
|
216
|
+
const value = editor.getControl().getValue();
|
|
217
|
+
setIsInputEmpty(!value || value.length === 0);
|
|
218
|
+
updateEditorHeight();
|
|
219
|
+
handleOnChange();
|
|
220
|
+
});
|
|
221
|
+
const resizeObserver = new ResizeObserver(updateEditorHeight);
|
|
222
|
+
if (editorContainerRef.current) {
|
|
223
|
+
resizeObserver.observe(editorContainerRef.current);
|
|
224
|
+
}
|
|
225
|
+
editor.getControl().onDidDispose(() => {
|
|
226
|
+
resizeObserver.disconnect();
|
|
227
|
+
});
|
|
171
228
|
|
|
172
|
-
|
|
229
|
+
editor.getControl().onContextMenu(e =>
|
|
230
|
+
props.contextMenuCallback(e.event)
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
editorRef.current = editor;
|
|
234
|
+
props.setEditorRef(editor);
|
|
235
|
+
};
|
|
173
236
|
createInputElement();
|
|
174
237
|
return () => {
|
|
238
|
+
props.setEditorRef(undefined);
|
|
175
239
|
if (editorRef.current) {
|
|
176
240
|
editorRef.current.dispose();
|
|
177
241
|
}
|
|
178
242
|
};
|
|
179
243
|
}, []);
|
|
180
244
|
|
|
245
|
+
const responseListenerRef = React.useRef<Disposable>();
|
|
246
|
+
// track chat model updates to keep our UI in sync
|
|
247
|
+
// - keep "inProgress" in sync with the request state
|
|
248
|
+
// - keep "changeSetUI" in sync with the change set
|
|
181
249
|
React.useEffect(() => {
|
|
182
|
-
const listener =
|
|
183
|
-
if (
|
|
184
|
-
|
|
250
|
+
const listener = props.chatModel.onDidChange(event => {
|
|
251
|
+
if (event.kind === 'addRequest') {
|
|
252
|
+
if (event.request) {
|
|
253
|
+
setInProgress(ChatRequestModel.isInProgress(event.request));
|
|
254
|
+
}
|
|
255
|
+
responseListenerRef.current?.dispose();
|
|
256
|
+
responseListenerRef.current = event.request.response.onDidChange(() =>
|
|
257
|
+
setInProgress(ChatRequestModel.isInProgress(event.request))
|
|
258
|
+
);
|
|
259
|
+
} else if (ChatChangeEvent.isChangeSetEvent(event)) {
|
|
260
|
+
if (event.changeSet) {
|
|
261
|
+
setChangeSetUI(buildChangeSetUI(event.changeSet, props.labelProvider, onDeleteChangeSet, onDeleteChangeSetElement));
|
|
262
|
+
} else {
|
|
263
|
+
setChangeSetUI(undefined);
|
|
264
|
+
}
|
|
185
265
|
}
|
|
186
266
|
});
|
|
187
|
-
|
|
188
|
-
|
|
267
|
+
setChangeSetUI(props.chatModel.changeSet ? buildChangeSetUI(props.chatModel.changeSet, props.labelProvider, onDeleteChangeSet, onDeleteChangeSetElement) : undefined);
|
|
268
|
+
return () => {
|
|
269
|
+
listener?.dispose();
|
|
270
|
+
responseListenerRef.current?.dispose();
|
|
271
|
+
responseListenerRef.current = undefined;
|
|
272
|
+
};
|
|
273
|
+
}, [props.chatModel]);
|
|
189
274
|
|
|
190
275
|
function submit(value: string): void {
|
|
276
|
+
if (!value || value.trim().length === 0) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
191
279
|
setInProgress(true);
|
|
192
280
|
props.onQuery(value);
|
|
193
281
|
if (editorRef.current) {
|
|
194
282
|
editorRef.current.document.textEditorModel.setValue('');
|
|
195
283
|
}
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
function layout(): void {
|
|
199
|
-
if (editorRef.current === undefined) {
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
const hiddenClass = 'hidden';
|
|
203
|
-
const editor = editorRef.current;
|
|
204
|
-
if (editor.document.textEditorModel.getValue().length > 0) {
|
|
205
|
-
placeholderRef.current?.classList.add(hiddenClass);
|
|
206
|
-
} else {
|
|
207
|
-
placeholderRef.current?.classList.remove(hiddenClass);
|
|
208
|
-
}
|
|
209
284
|
}
|
|
210
285
|
|
|
211
286
|
const onKeyDown = React.useCallback((event: React.KeyboardEvent) => {
|
|
@@ -218,30 +293,207 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
218
293
|
}
|
|
219
294
|
}, [props.isEnabled]);
|
|
220
295
|
|
|
296
|
+
const handleInputFocus = () => {
|
|
297
|
+
hidePlaceholderIfEditorFilled();
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const handleOnChange = () => {
|
|
301
|
+
showPlaceholderIfEditorEmpty();
|
|
302
|
+
hidePlaceholderIfEditorFilled();
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const handleInputBlur = () => {
|
|
306
|
+
showPlaceholderIfEditorEmpty();
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const showPlaceholderIfEditorEmpty = () => {
|
|
310
|
+
if (!editorRef.current?.getControl().getValue()) {
|
|
311
|
+
placeholderRef.current?.classList.remove('hidden');
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const hidePlaceholderIfEditorFilled = () => {
|
|
316
|
+
const value = editorRef.current?.getControl().getValue();
|
|
317
|
+
if (value && value.length > 0) {
|
|
318
|
+
placeholderRef.current?.classList.add('hidden');
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const leftOptions = props.showContext ? [{
|
|
323
|
+
title: 'Attach elements to context',
|
|
324
|
+
handler: () => { /* TODO */ },
|
|
325
|
+
className: 'codicon-add'
|
|
326
|
+
}] : [];
|
|
327
|
+
|
|
328
|
+
const rightOptions = inProgress
|
|
329
|
+
? [{
|
|
330
|
+
title: 'Cancel (Esc)',
|
|
331
|
+
handler: () => {
|
|
332
|
+
const latestRequest = getLatestRequest(props.chatModel);
|
|
333
|
+
if (latestRequest) {
|
|
334
|
+
props.onCancel(latestRequest);
|
|
335
|
+
}
|
|
336
|
+
setInProgress(false);
|
|
337
|
+
},
|
|
338
|
+
className: 'codicon-stop-circle'
|
|
339
|
+
}]
|
|
340
|
+
: [{
|
|
341
|
+
title: 'Send (Enter)',
|
|
342
|
+
handler: () => {
|
|
343
|
+
if (props.isEnabled) {
|
|
344
|
+
submit(editorRef.current?.document.textEditorModel.getValue() || '');
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
className: 'codicon-send',
|
|
348
|
+
disabled: isInputEmpty || !props.isEnabled
|
|
349
|
+
}];
|
|
350
|
+
|
|
221
351
|
return <div className='theia-ChatInput'>
|
|
352
|
+
{changeSetUI?.elements &&
|
|
353
|
+
<ChangeSetBox changeSet={changeSetUI} />
|
|
354
|
+
}
|
|
222
355
|
<div className='theia-ChatInput-Editor-Box'>
|
|
223
|
-
<div className='theia-ChatInput-Editor' ref={editorContainerRef} onKeyDown={onKeyDown}>
|
|
224
|
-
<div ref={placeholderRef} className='theia-ChatInput-Editor-Placeholder'>
|
|
356
|
+
<div className='theia-ChatInput-Editor' ref={editorContainerRef} onKeyDown={onKeyDown} onFocus={handleInputFocus} onBlur={handleInputBlur}>
|
|
357
|
+
<div ref={placeholderRef} className='theia-ChatInput-Editor-Placeholder'>Ask a question</div>
|
|
225
358
|
</div>
|
|
226
|
-
|
|
227
|
-
<div className="theia-ChatInputOptions">
|
|
228
|
-
{
|
|
229
|
-
inProgress ? <span
|
|
230
|
-
className="codicon codicon-stop-circle option"
|
|
231
|
-
title="Cancel (Esc)"
|
|
232
|
-
onClick={() => {
|
|
233
|
-
if (lastRequest) {
|
|
234
|
-
props.onCancel(lastRequest);
|
|
235
|
-
}
|
|
236
|
-
setInProgress(false);
|
|
237
|
-
}} /> :
|
|
238
|
-
<span
|
|
239
|
-
className="codicon codicon-send option"
|
|
240
|
-
title="Send (Enter)"
|
|
241
|
-
onClick={!props.isEnabled ? undefined : () => submit(editorRef.current?.document.textEditorModel.getValue() || '')}
|
|
242
|
-
style={{ cursor: !props.isEnabled ? 'default' : 'pointer', opacity: !props.isEnabled ? 0.5 : 1 }}
|
|
243
|
-
/>
|
|
244
|
-
}
|
|
359
|
+
<ChatInputOptions leftOptions={leftOptions} rightOptions={rightOptions} />
|
|
245
360
|
</div>
|
|
246
361
|
</div>;
|
|
247
362
|
};
|
|
363
|
+
|
|
364
|
+
const noPropagation = (handler: () => void) => (e: React.MouseEvent) => {
|
|
365
|
+
handler();
|
|
366
|
+
e.stopPropagation();
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const buildChangeSetUI = (changeSet: ChangeSet, labelProvider: LabelProvider, onDeleteChangeSet: () => void, onDeleteChangeSetElement: (index: number) => void): ChangeSetUI => ({
|
|
370
|
+
title: changeSet.title,
|
|
371
|
+
disabled: !hasPendingElementsToAccept(changeSet),
|
|
372
|
+
acceptAllPendingElements: () => acceptAllPendingElements(changeSet),
|
|
373
|
+
delete: () => onDeleteChangeSet(),
|
|
374
|
+
elements: changeSet.getElements().map(element => ({
|
|
375
|
+
open: element?.open?.bind(element),
|
|
376
|
+
iconClass: element.icon ?? labelProvider.getIcon(element.uri) ?? labelProvider.fileIcon,
|
|
377
|
+
nameClass: `${element.type} ${element.state}`,
|
|
378
|
+
name: element.name ?? labelProvider.getName(element.uri),
|
|
379
|
+
additionalInfo: element.additionalInfo ?? labelProvider.getDetails(element.uri),
|
|
380
|
+
openChange: element?.openChange?.bind(element),
|
|
381
|
+
accept: element.state !== 'applied' ? element?.accept?.bind(element) : undefined,
|
|
382
|
+
discard: element.state === 'applied' ? element?.discard?.bind(element) : undefined,
|
|
383
|
+
delete: () => onDeleteChangeSetElement(changeSet.getElements().indexOf(element))
|
|
384
|
+
}))
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
interface ChangeSetUIElement {
|
|
388
|
+
name: string;
|
|
389
|
+
iconClass: string;
|
|
390
|
+
nameClass: string;
|
|
391
|
+
additionalInfo: string;
|
|
392
|
+
open?: () => void;
|
|
393
|
+
openChange?: () => void;
|
|
394
|
+
accept?: () => void;
|
|
395
|
+
discard?: () => void;
|
|
396
|
+
delete: () => void;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
interface ChangeSetUI {
|
|
400
|
+
title: string;
|
|
401
|
+
disabled: boolean;
|
|
402
|
+
acceptAllPendingElements: () => void;
|
|
403
|
+
delete: () => void;
|
|
404
|
+
elements: ChangeSetUIElement[];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const ChangeSetBox: React.FunctionComponent<{ changeSet: ChangeSetUI }> = ({ changeSet }) => (
|
|
408
|
+
<div className='theia-ChatInput-ChangeSet-Box'>
|
|
409
|
+
<div className='theia-ChatInput-ChangeSet-Header'>
|
|
410
|
+
<h3>{changeSet.title}</h3>
|
|
411
|
+
<div className='theia-ChatInput-ChangeSet-Header-Actions'>
|
|
412
|
+
<button
|
|
413
|
+
className='theia-button'
|
|
414
|
+
disabled={changeSet.disabled}
|
|
415
|
+
title='Accept all pending changes'
|
|
416
|
+
onClick={() => changeSet.acceptAllPendingElements()}
|
|
417
|
+
>
|
|
418
|
+
Accept
|
|
419
|
+
</button>
|
|
420
|
+
<span className='codicon codicon-close action' title='Delete Change Set' onClick={() => changeSet.delete()} />
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
<div className='theia-ChatInput-ChangeSet-List'>
|
|
424
|
+
<ul>
|
|
425
|
+
{changeSet.elements.map((element, index) => (
|
|
426
|
+
<li key={index} title='Open Diff' onClick={() => element.openChange?.()}>
|
|
427
|
+
<div className={`theia-ChatInput-ChangeSet-Icon ${element.iconClass}`} />
|
|
428
|
+
<span className={`theia-ChatInput-ChangeSet-title ${element.nameClass}`}>
|
|
429
|
+
{element.name}
|
|
430
|
+
</span>
|
|
431
|
+
<span className='theia-ChatInput-ChangeSet-additionalInfo'>
|
|
432
|
+
{element.additionalInfo}
|
|
433
|
+
</span>
|
|
434
|
+
<div className='theia-ChatInput-ChangeSet-Actions'>
|
|
435
|
+
{element.open && (<span className='codicon codicon-file action' title='Open Original File' onClick={noPropagation(() => element.open!())} />)}
|
|
436
|
+
{element.discard && (<span className='codicon codicon-discard action' title='Undo' onClick={noPropagation(() => element.discard!())} />)}
|
|
437
|
+
{element.accept && (<span className='codicon codicon-check action' title='Accept' onClick={noPropagation(() => element.accept!())} />)}
|
|
438
|
+
<span className='codicon codicon-close action' title='Delete' onClick={noPropagation(() => element.delete())} />
|
|
439
|
+
</div>
|
|
440
|
+
</li>
|
|
441
|
+
))}
|
|
442
|
+
</ul>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
interface ChatInputOptionsProps {
|
|
448
|
+
leftOptions: Option[];
|
|
449
|
+
rightOptions: Option[];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
interface Option {
|
|
453
|
+
title: string;
|
|
454
|
+
handler: () => void;
|
|
455
|
+
className: string;
|
|
456
|
+
disabled?: boolean;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const ChatInputOptions: React.FunctionComponent<ChatInputOptionsProps> = ({ leftOptions, rightOptions }) => (
|
|
460
|
+
<div className="theia-ChatInputOptions">
|
|
461
|
+
<div className="theia-ChatInputOptions-left">
|
|
462
|
+
{leftOptions.map((option, index) => (
|
|
463
|
+
<span
|
|
464
|
+
key={index}
|
|
465
|
+
className={`codicon ${option.className} option ${option.disabled ? 'disabled' : ''}`}
|
|
466
|
+
title={option.title}
|
|
467
|
+
onClick={option.handler}
|
|
468
|
+
/>
|
|
469
|
+
))}
|
|
470
|
+
</div>
|
|
471
|
+
<div className="theia-ChatInputOptions-right">
|
|
472
|
+
{rightOptions.map((option, index) => (
|
|
473
|
+
<span
|
|
474
|
+
key={index}
|
|
475
|
+
className={`codicon ${option.className} option ${option.disabled ? 'disabled' : ''}`}
|
|
476
|
+
title={option.title}
|
|
477
|
+
onClick={option.handler}
|
|
478
|
+
/>
|
|
479
|
+
))}
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
function acceptAllPendingElements(changeSet: ChangeSet): void {
|
|
485
|
+
acceptablePendingElements(changeSet).forEach(e => e.accept!());
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function hasPendingElementsToAccept(changeSet: ChangeSet): boolean | undefined {
|
|
489
|
+
return acceptablePendingElements(changeSet).length > 0;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function acceptablePendingElements(changeSet: ChangeSet): ChangeSetElement[] {
|
|
493
|
+
return changeSet.getElements().filter(e => e.accept && (e.state === undefined || e.state === 'pending'));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function getLatestRequest(chatModel: ChatModel): ChatRequestModel | undefined {
|
|
497
|
+
const requests = chatModel.getRequests();
|
|
498
|
+
return requests.length > 0 ? requests[requests.length - 1] : undefined;
|
|
499
|
+
}
|
|
@@ -13,15 +13,14 @@
|
|
|
13
13
|
//
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
|
-
|
|
17
16
|
import {
|
|
18
17
|
ChatResponseContent,
|
|
19
18
|
CodeChatResponseContent,
|
|
20
19
|
} from '@theia/ai-chat/lib/common';
|
|
21
|
-
import { UntitledResourceResolver, URI } from '@theia/core';
|
|
20
|
+
import { ContributionProvider, UntitledResourceResolver, URI } from '@theia/core';
|
|
22
21
|
import { ContextMenuRenderer, TreeNode } from '@theia/core/lib/browser';
|
|
23
22
|
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
|
|
24
|
-
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
23
|
+
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
|
25
24
|
import * as React from '@theia/core/shared/react';
|
|
26
25
|
import { ReactNode } from '@theia/core/shared/react';
|
|
27
26
|
import { Position } from '@theia/core/shared/vscode-languageserver-protocol';
|
|
@@ -33,12 +32,29 @@ import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
|
|
|
33
32
|
import { ChatViewTreeWidget, ResponseNode } from '../chat-tree-view/chat-view-tree-widget';
|
|
34
33
|
import { IMouseEvent } from '@theia/monaco-editor-core';
|
|
35
34
|
|
|
35
|
+
export const CodePartRendererAction = Symbol('CodePartRendererAction');
|
|
36
|
+
/**
|
|
37
|
+
* The CodePartRenderer offers to contribute arbitrary React nodes to the rendered code part.
|
|
38
|
+
* Technically anything can be rendered, however it is intended to be used for actions, like
|
|
39
|
+
* "Copy to Clipboard" or "Insert at Cursor".
|
|
40
|
+
*/
|
|
41
|
+
export interface CodePartRendererAction {
|
|
42
|
+
render(response: CodeChatResponseContent, parentNode: ResponseNode): ReactNode;
|
|
43
|
+
/**
|
|
44
|
+
* Determines if the action should be rendered for the given response.
|
|
45
|
+
*/
|
|
46
|
+
canRender?(response: CodeChatResponseContent, parentNode: ResponseNode): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* The priority determines the order in which the actions are rendered.
|
|
49
|
+
* The default priorities are 10 and 20.
|
|
50
|
+
*/
|
|
51
|
+
priority: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
36
54
|
@injectable()
|
|
37
55
|
export class CodePartRenderer
|
|
38
56
|
implements ChatResponsePartRenderer<CodeChatResponseContent> {
|
|
39
57
|
|
|
40
|
-
@inject(ClipboardService)
|
|
41
|
-
protected readonly clipboardService: ClipboardService;
|
|
42
58
|
@inject(EditorManager)
|
|
43
59
|
protected readonly editorManager: EditorManager;
|
|
44
60
|
@inject(UntitledResourceResolver)
|
|
@@ -49,6 +65,8 @@ export class CodePartRenderer
|
|
|
49
65
|
protected readonly languageService: MonacoLanguages;
|
|
50
66
|
@inject(ContextMenuRenderer)
|
|
51
67
|
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
|
68
|
+
@inject(ContributionProvider) @named(CodePartRendererAction)
|
|
69
|
+
protected readonly codePartRendererActions: ContributionProvider<CodePartRendererAction>;
|
|
52
70
|
|
|
53
71
|
canHandle(response: ChatResponseContent): number {
|
|
54
72
|
if (CodeChatResponseContent.is(response)) {
|
|
@@ -59,14 +77,15 @@ export class CodePartRenderer
|
|
|
59
77
|
|
|
60
78
|
render(response: CodeChatResponseContent, parentNode: ResponseNode): ReactNode {
|
|
61
79
|
const language = response.language ? this.languageService.getExtension(response.language) : undefined;
|
|
62
|
-
|
|
63
80
|
return (
|
|
64
81
|
<div className="theia-CodePartRenderer-root">
|
|
65
82
|
<div className="theia-CodePartRenderer-top">
|
|
66
83
|
<div className="theia-CodePartRenderer-left">{this.renderTitle(response)}</div>
|
|
67
|
-
<div className="theia-CodePartRenderer-right">
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
<div className="theia-CodePartRenderer-right theia-CodePartRenderer-actions">
|
|
85
|
+
{this.codePartRendererActions.getContributions()
|
|
86
|
+
.filter(action => action.canRender ? action.canRender(response, parentNode) : true)
|
|
87
|
+
.sort((a, b) => a.priority - b.priority)
|
|
88
|
+
.map(action => action.render(response, parentNode))}
|
|
70
89
|
</div>
|
|
71
90
|
</div>
|
|
72
91
|
<div className="theia-CodePartRenderer-separator"></div>
|
|
@@ -123,6 +142,16 @@ export class CodePartRenderer
|
|
|
123
142
|
}
|
|
124
143
|
}
|
|
125
144
|
|
|
145
|
+
@injectable()
|
|
146
|
+
export class CopyToClipboardButtonAction implements CodePartRendererAction {
|
|
147
|
+
@inject(ClipboardService)
|
|
148
|
+
protected readonly clipboardService: ClipboardService;
|
|
149
|
+
priority = 10;
|
|
150
|
+
render(response: CodeChatResponseContent): ReactNode {
|
|
151
|
+
return <CopyToClipboardButton key='copyToClipBoard' code={response.code} clipboardService={this.clipboardService} />;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
126
155
|
const CopyToClipboardButton = (props: { code: string, clipboardService: ClipboardService }) => {
|
|
127
156
|
const { code, clipboardService } = props;
|
|
128
157
|
const copyCodeToClipboard = React.useCallback(() => {
|
|
@@ -131,6 +160,16 @@ const CopyToClipboardButton = (props: { code: string, clipboardService: Clipboar
|
|
|
131
160
|
return <div className='button codicon codicon-copy' title='Copy' role='button' onClick={copyCodeToClipboard}></div>;
|
|
132
161
|
};
|
|
133
162
|
|
|
163
|
+
@injectable()
|
|
164
|
+
export class InsertCodeAtCursorButtonAction implements CodePartRendererAction {
|
|
165
|
+
@inject(EditorManager)
|
|
166
|
+
protected readonly editorManager: EditorManager;
|
|
167
|
+
priority = 20;
|
|
168
|
+
render(response: CodeChatResponseContent): ReactNode {
|
|
169
|
+
return <InsertCodeAtCursorButton key='insertCodeAtCursor' code={response.code} editorManager={this.editorManager} />;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
134
173
|
const InsertCodeAtCursorButton = (props: { code: string, editorManager: EditorManager }) => {
|
|
135
174
|
const { code, editorManager } = props;
|
|
136
175
|
const insertCode = React.useCallback(() => {
|