@theia/ai-chat-ui 1.56.0 → 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-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-chat-ui-frontend-module.js +9 -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 +228 -80
- 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 +8 -3
- package/lib/browser/chat-response-renderer/markdown-part-renderer.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/markdown-part-renderer.js +38 -10
- package/lib/browser/chat-response-renderer/markdown-part-renderer.js.map +1 -1
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +8 -2
- 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 +2 -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 +8 -4
- 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-frontend-module.ts +27 -5
- package/src/browser/chat-input-widget.tsx +337 -100
- package/src/browser/chat-response-renderer/code-part-renderer.tsx +48 -9
- package/src/browser/chat-response-renderer/markdown-part-renderer.tsx +39 -12
- package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +8 -2
- package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +10 -4
- 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 +186 -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,145 @@ 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 paddingTop = 8;
|
|
130
|
-
const lineHeight = 20;
|
|
131
|
-
const maxHeight = 240;
|
|
132
|
-
const resource = await props.untitledResourceResolver.createUntitledResource('', CHAT_VIEW_LANGUAGE_EXTENSION);
|
|
133
|
-
const editor = await props.editorProvider.createInline(resource.uri, editorContainerRef.current!, {
|
|
134
|
-
language: CHAT_VIEW_LANGUAGE_EXTENSION,
|
|
135
|
-
// Disable code lens, inlay hints and hover support to avoid console errors from other contributions
|
|
136
|
-
codeLens: false,
|
|
137
|
-
inlayHints: { enabled: 'off' },
|
|
138
|
-
hover: { enabled: false },
|
|
139
|
-
autoSizing: false, // we handle the sizing ourselves
|
|
140
|
-
scrollBeyondLastLine: false,
|
|
141
|
-
scrollBeyondLastColumn: 0,
|
|
142
|
-
minHeight: 1,
|
|
143
|
-
fontFamily: 'var(--theia-ui-font-family)',
|
|
144
|
-
fontSize: 13,
|
|
145
|
-
cursorWidth: 1,
|
|
146
|
-
maxHeight: -1,
|
|
147
|
-
scrollbar: { horizontal: 'hidden' },
|
|
148
|
-
automaticLayout: true,
|
|
149
|
-
lineNumbers: 'off',
|
|
150
|
-
lineHeight,
|
|
151
|
-
padding: { top: paddingTop },
|
|
152
|
-
suggest: {
|
|
153
|
-
showIcons: true,
|
|
154
|
-
showSnippets: false,
|
|
155
|
-
showWords: false,
|
|
156
|
-
showStatusBar: false,
|
|
157
|
-
insertMode: 'replace',
|
|
158
|
-
},
|
|
159
|
-
bracketPairColorization: { enabled: false },
|
|
160
|
-
wrappingStrategy: 'advanced',
|
|
161
|
-
stickyScroll: { enabled: false },
|
|
162
|
-
});
|
|
163
167
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
});
|
|
167
204
|
|
|
168
|
-
const updateEditorHeight = () => {
|
|
169
205
|
if (editorContainerRef.current) {
|
|
170
|
-
|
|
171
|
-
editorContainerRef.current.style.height = `${Math.min(contentHeight, maxHeight)}px`;
|
|
206
|
+
editorContainerRef.current.style.height = (lineHeight + (2 * paddingTop)) + 'px';
|
|
172
207
|
}
|
|
173
|
-
};
|
|
174
|
-
editor.getControl().onDidChangeModelContent(updateEditorHeight);
|
|
175
|
-
const resizeObserver = new ResizeObserver(updateEditorHeight);
|
|
176
|
-
if (editorContainerRef.current) {
|
|
177
|
-
resizeObserver.observe(editorContainerRef.current);
|
|
178
|
-
}
|
|
179
|
-
editor.getControl().onDidDispose(() => {
|
|
180
|
-
resizeObserver.disconnect();
|
|
181
|
-
});
|
|
182
208
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
+
});
|
|
186
228
|
|
|
187
|
-
|
|
188
|
-
|
|
229
|
+
editor.getControl().onContextMenu(e =>
|
|
230
|
+
props.contextMenuCallback(e.event)
|
|
231
|
+
);
|
|
189
232
|
|
|
190
|
-
|
|
233
|
+
editorRef.current = editor;
|
|
234
|
+
props.setEditorRef(editor);
|
|
235
|
+
};
|
|
191
236
|
createInputElement();
|
|
192
237
|
return () => {
|
|
238
|
+
props.setEditorRef(undefined);
|
|
193
239
|
if (editorRef.current) {
|
|
194
240
|
editorRef.current.dispose();
|
|
195
241
|
}
|
|
196
242
|
};
|
|
197
243
|
}, []);
|
|
198
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
|
|
199
249
|
React.useEffect(() => {
|
|
200
|
-
const listener =
|
|
201
|
-
if (
|
|
202
|
-
|
|
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
|
+
}
|
|
203
265
|
}
|
|
204
266
|
});
|
|
205
|
-
|
|
206
|
-
|
|
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]);
|
|
207
274
|
|
|
208
275
|
function submit(value: string): void {
|
|
276
|
+
if (!value || value.trim().length === 0) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
209
279
|
setInProgress(true);
|
|
210
280
|
props.onQuery(value);
|
|
211
281
|
if (editorRef.current) {
|
|
@@ -224,39 +294,206 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
224
294
|
}, [props.isEnabled]);
|
|
225
295
|
|
|
226
296
|
const handleInputFocus = () => {
|
|
227
|
-
|
|
297
|
+
hidePlaceholderIfEditorFilled();
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const handleOnChange = () => {
|
|
301
|
+
showPlaceholderIfEditorEmpty();
|
|
302
|
+
hidePlaceholderIfEditorFilled();
|
|
228
303
|
};
|
|
229
304
|
|
|
230
305
|
const handleInputBlur = () => {
|
|
306
|
+
showPlaceholderIfEditorEmpty();
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const showPlaceholderIfEditorEmpty = () => {
|
|
231
310
|
if (!editorRef.current?.getControl().getValue()) {
|
|
232
311
|
placeholderRef.current?.classList.remove('hidden');
|
|
233
312
|
}
|
|
234
313
|
};
|
|
235
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
|
+
|
|
236
351
|
return <div className='theia-ChatInput'>
|
|
352
|
+
{changeSetUI?.elements &&
|
|
353
|
+
<ChangeSetBox changeSet={changeSetUI} />
|
|
354
|
+
}
|
|
237
355
|
<div className='theia-ChatInput-Editor-Box'>
|
|
238
356
|
<div className='theia-ChatInput-Editor' ref={editorContainerRef} onKeyDown={onKeyDown} onFocus={handleInputFocus} onBlur={handleInputBlur}>
|
|
239
357
|
<div ref={placeholderRef} className='theia-ChatInput-Editor-Placeholder'>Ask a question</div>
|
|
240
358
|
</div>
|
|
241
|
-
|
|
242
|
-
<div className="theia-ChatInputOptions">
|
|
243
|
-
{
|
|
244
|
-
inProgress ? <span
|
|
245
|
-
className="codicon codicon-stop-circle option"
|
|
246
|
-
title="Cancel (Esc)"
|
|
247
|
-
onClick={() => {
|
|
248
|
-
if (lastRequest) {
|
|
249
|
-
props.onCancel(lastRequest);
|
|
250
|
-
}
|
|
251
|
-
setInProgress(false);
|
|
252
|
-
}} /> :
|
|
253
|
-
<span
|
|
254
|
-
className="codicon codicon-send option"
|
|
255
|
-
title="Send (Enter)"
|
|
256
|
-
onClick={!props.isEnabled ? undefined : () => submit(editorRef.current?.document.textEditorModel.getValue() || '')}
|
|
257
|
-
style={{ cursor: !props.isEnabled ? 'default' : 'pointer', opacity: !props.isEnabled ? 0.5 : 1 }}
|
|
258
|
-
/>
|
|
259
|
-
}
|
|
359
|
+
<ChatInputOptions leftOptions={leftOptions} rightOptions={rightOptions} />
|
|
260
360
|
</div>
|
|
261
361
|
</div>;
|
|
262
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(() => {
|