@theia/ai-chat-ui 1.63.0-next.0 → 1.63.0-next.52
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 +29 -1
- package/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -1
- package/lib/browser/ai-chat-ui-contribution.js +158 -2
- 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 +8 -0
- package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -1
- package/lib/browser/chat-input-widget.d.ts +9 -6
- package/lib/browser/chat-input-widget.d.ts.map +1 -1
- package/lib/browser/chat-input-widget.js +181 -111
- package/lib/browser/chat-input-widget.js.map +1 -1
- package/lib/browser/chat-node-toolbar-action-contribution.d.ts +1 -0
- package/lib/browser/chat-node-toolbar-action-contribution.d.ts.map +1 -1
- package/lib/browser/chat-node-toolbar-action-contribution.js +13 -0
- package/lib/browser/chat-node-toolbar-action-contribution.js.map +1 -1
- package/lib/browser/chat-response-renderer/delegation-response-renderer.d.ts +14 -0
- package/lib/browser/chat-response-renderer/delegation-response-renderer.d.ts.map +1 -0
- package/lib/browser/chat-response-renderer/delegation-response-renderer.js +144 -0
- package/lib/browser/chat-response-renderer/delegation-response-renderer.js.map +1 -0
- package/lib/browser/chat-response-renderer/index.d.ts +2 -0
- package/lib/browser/chat-response-renderer/index.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/index.js +2 -0
- package/lib/browser/chat-response-renderer/index.js.map +1 -1
- package/lib/browser/chat-response-renderer/tool-confirmation.d.ts +17 -0
- package/lib/browser/chat-response-renderer/tool-confirmation.d.ts.map +1 -0
- package/lib/browser/chat-response-renderer/tool-confirmation.js +120 -0
- package/lib/browser/chat-response-renderer/tool-confirmation.js.map +1 -0
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts +5 -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 +83 -19
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
- package/lib/browser/chat-tree-view/chat-view-tree-input-widget.d.ts +6 -1
- package/lib/browser/chat-tree-view/chat-view-tree-input-widget.d.ts.map +1 -1
- package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js +9 -0
- package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js.map +1 -1
- package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts +8 -0
- 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 +72 -3
- package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
- package/lib/browser/chat-tree-view/sub-chat-widget.d.ts +22 -0
- package/lib/browser/chat-tree-view/sub-chat-widget.d.ts.map +1 -0
- package/lib/browser/chat-tree-view/sub-chat-widget.js +92 -0
- package/lib/browser/chat-tree-view/sub-chat-widget.js.map +1 -0
- package/lib/browser/chat-view-commands.d.ts +1 -0
- package/lib/browser/chat-view-commands.d.ts.map +1 -1
- package/lib/browser/chat-view-commands.js +5 -0
- package/lib/browser/chat-view-commands.js.map +1 -1
- package/lib/browser/chat-view-contribution.js +2 -1
- package/lib/browser/chat-view-contribution.js.map +1 -1
- package/lib/browser/chat-view-widget.d.ts +6 -3
- package/lib/browser/chat-view-widget.d.ts.map +1 -1
- package/lib/browser/chat-view-widget.js +20 -10
- package/lib/browser/chat-view-widget.js.map +1 -1
- package/package.json +10 -10
- package/src/browser/ai-chat-ui-contribution.ts +166 -5
- package/src/browser/ai-chat-ui-frontend-module.ts +11 -0
- package/src/browser/chat-input-widget.tsx +280 -170
- package/src/browser/chat-node-toolbar-action-contribution.ts +14 -0
- package/src/browser/chat-response-renderer/delegation-response-renderer.tsx +177 -0
- package/src/browser/chat-response-renderer/index.ts +2 -0
- package/src/browser/chat-response-renderer/tool-confirmation.tsx +173 -0
- package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +115 -19
- package/src/browser/chat-tree-view/chat-view-tree-input-widget.tsx +16 -1
- package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +89 -5
- package/src/browser/chat-tree-view/sub-chat-widget.tsx +101 -0
- package/src/browser/chat-view-commands.ts +6 -0
- package/src/browser/chat-view-contribution.ts +1 -1
- package/src/browser/chat-view-widget.tsx +25 -12
- package/src/browser/style/index.css +350 -2
|
@@ -13,22 +13,26 @@
|
|
|
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
|
-
|
|
16
|
+
import {
|
|
17
|
+
ChangeSet, ChangeSetElement, ChatAgent, ChatChangeEvent, ChatHierarchyBranch,
|
|
18
|
+
ChatModel, ChatRequestModel, ChatService, ChatSuggestion, EditableChatRequestModel
|
|
19
|
+
} from '@theia/ai-chat';
|
|
20
|
+
import { ChangeSetDecoratorService } from '@theia/ai-chat/lib/browser/change-set-decorator-service';
|
|
21
|
+
import { ImageContextVariable } from '@theia/ai-chat/lib/common/image-context-variable';
|
|
22
|
+
import { AIVariableResolutionRequest } from '@theia/ai-core';
|
|
23
|
+
import { FrontendVariableService } from '@theia/ai-core/lib/browser';
|
|
24
|
+
import { DisposableCollection, InMemoryResources, URI, nls } from '@theia/core';
|
|
18
25
|
import { ContextMenuRenderer, LabelProvider, Message, OpenerService, ReactWidget } from '@theia/core/lib/browser';
|
|
19
26
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
20
27
|
import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify';
|
|
21
28
|
import * as React from '@theia/core/shared/react';
|
|
22
29
|
import { IMouseEvent } from '@theia/monaco-editor-core';
|
|
23
|
-
import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
|
|
24
30
|
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
|
|
25
|
-
import {
|
|
26
|
-
import { AIVariableResolutionRequest } from '@theia/ai-core';
|
|
27
|
-
import { FrontendVariableService } from '@theia/ai-core/lib/browser';
|
|
28
|
-
import { ContextVariablePicker } from './context-variable-picker';
|
|
31
|
+
import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
|
|
29
32
|
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
33
|
import { ChatInputAgentSuggestions } from './chat-input-agent-suggestions';
|
|
34
|
+
import { CHAT_VIEW_LANGUAGE_EXTENSION } from './chat-view-language-contribution';
|
|
35
|
+
import { ContextVariablePicker } from './context-variable-picker';
|
|
32
36
|
|
|
33
37
|
type Query = (query: string) => Promise<void>;
|
|
34
38
|
type Unpin = () => void;
|
|
@@ -88,6 +92,14 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
88
92
|
|
|
89
93
|
protected isEnabled = false;
|
|
90
94
|
|
|
95
|
+
private _branch?: ChatHierarchyBranch;
|
|
96
|
+
set branch(branch: ChatHierarchyBranch | undefined) {
|
|
97
|
+
if (this._branch !== branch) {
|
|
98
|
+
this._branch = branch;
|
|
99
|
+
this.update();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
91
103
|
private _onQuery: Query;
|
|
92
104
|
set onQuery(query: Query) {
|
|
93
105
|
this._onQuery = query;
|
|
@@ -120,7 +132,7 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
120
132
|
this.onDisposeForChatModel.dispose();
|
|
121
133
|
this.onDisposeForChatModel = new DisposableCollection();
|
|
122
134
|
this.onDisposeForChatModel.push(chatModel.onDidChange(event => {
|
|
123
|
-
if (event.kind === 'addVariable' || event.kind === 'removeVariable') {
|
|
135
|
+
if (event.kind === 'addVariable' || event.kind === 'removeVariable' || event.kind === 'addRequest' || event.kind === 'changeHierarchyBranch') {
|
|
124
136
|
this.update();
|
|
125
137
|
}
|
|
126
138
|
}));
|
|
@@ -137,6 +149,7 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
137
149
|
protected init(): void {
|
|
138
150
|
this.id = AIChatInputWidget.ID;
|
|
139
151
|
this.title.closable = false;
|
|
152
|
+
this.toDispose.push(this.resources.add(this.getResourceUri(), ''));
|
|
140
153
|
this.update();
|
|
141
154
|
}
|
|
142
155
|
|
|
@@ -154,24 +167,36 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
154
167
|
}
|
|
155
168
|
|
|
156
169
|
protected render(): React.ReactNode {
|
|
170
|
+
const branch = this._branch;
|
|
171
|
+
const chatModel = this._chatModel;
|
|
172
|
+
|
|
173
|
+
// State of the input widget's action buttons depends on the state of the currently active or last processed
|
|
174
|
+
// request, if there is one. If the chat model has branched, then the current request is the last on the
|
|
175
|
+
// branch. Otherwise, it's the last request in the chat model.
|
|
176
|
+
const currentRequest: ChatRequestModel | undefined = branch?.items?.at(-1)?.element ?? chatModel.getRequests().at(-1);
|
|
177
|
+
const isEditing = !!(currentRequest && (EditableChatRequestModel.isEditing(currentRequest)));
|
|
178
|
+
const isPending = () => !!(currentRequest && !isEditing && ChatRequestModel.isInProgress(currentRequest));
|
|
179
|
+
const pending = isPending();
|
|
180
|
+
|
|
157
181
|
return (
|
|
158
182
|
<ChatInput
|
|
183
|
+
branch={this._branch}
|
|
159
184
|
onQuery={this._onQuery.bind(this)}
|
|
160
185
|
onUnpin={this._onUnpin.bind(this)}
|
|
161
186
|
onCancel={this._onCancel.bind(this)}
|
|
162
187
|
onDragOver={this.onDragOver.bind(this)}
|
|
163
188
|
onDrop={this.onDrop.bind(this)}
|
|
189
|
+
onPaste={this.onPaste.bind(this)}
|
|
164
190
|
onDeleteChangeSet={this._onDeleteChangeSet.bind(this)}
|
|
165
191
|
onDeleteChangeSetElement={this._onDeleteChangeSetElement.bind(this)}
|
|
166
192
|
onAddContextElement={this.addContextElement.bind(this)}
|
|
167
193
|
onDeleteContextElement={this.deleteContextElement.bind(this)}
|
|
168
|
-
context={this.getContext()}
|
|
169
194
|
onOpenContextElement={this.openContextElement.bind(this)}
|
|
195
|
+
context={this.getContext()}
|
|
170
196
|
chatModel={this._chatModel}
|
|
171
197
|
pinnedAgent={this._pinnedAgent}
|
|
172
198
|
editorProvider={this.editorProvider}
|
|
173
|
-
|
|
174
|
-
resourceUriProvider={this.getResourceUri.bind(this)}
|
|
199
|
+
uri={this.getResourceUri()}
|
|
175
200
|
contextMenuCallback={this.handleContextMenu.bind(this)}
|
|
176
201
|
isEnabled={this.isEnabled}
|
|
177
202
|
setEditorRef={editor => {
|
|
@@ -188,6 +213,14 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
188
213
|
initialValue={this._initialValue}
|
|
189
214
|
openerService={this.openerService}
|
|
190
215
|
suggestions={this._chatModel.suggestions}
|
|
216
|
+
currentRequest={currentRequest}
|
|
217
|
+
isEditing={isEditing}
|
|
218
|
+
pending={pending}
|
|
219
|
+
onResponseChanged={() => {
|
|
220
|
+
if (isPending() !== pending) {
|
|
221
|
+
this.update();
|
|
222
|
+
}
|
|
223
|
+
}}
|
|
191
224
|
/>
|
|
192
225
|
);
|
|
193
226
|
}
|
|
@@ -226,6 +259,26 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
226
259
|
});
|
|
227
260
|
}
|
|
228
261
|
|
|
262
|
+
protected onPaste(event: ClipboardEvent): void {
|
|
263
|
+
this.variableService.getPasteResult(event, { type: 'ai-chat-input-widget' }).then(result => {
|
|
264
|
+
result.variables.forEach(variable => this.addContext(variable));
|
|
265
|
+
if (result.text) {
|
|
266
|
+
const position = this.editorRef?.getControl().getPosition();
|
|
267
|
+
if (position && result.text) {
|
|
268
|
+
this.editorRef?.getControl().executeEdits('paste', [{
|
|
269
|
+
range: {
|
|
270
|
+
startLineNumber: position.lineNumber,
|
|
271
|
+
startColumn: position.column,
|
|
272
|
+
endLineNumber: position.lineNumber,
|
|
273
|
+
endColumn: position.column
|
|
274
|
+
},
|
|
275
|
+
text: result.text
|
|
276
|
+
}]);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
229
282
|
protected async openContextElement(request: AIVariableResolutionRequest): Promise<void> {
|
|
230
283
|
const session = this.chatService.getSessions().find(candidate => candidate.model.id === this._chatModel.id);
|
|
231
284
|
const context = { session };
|
|
@@ -269,13 +322,15 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
269
322
|
}
|
|
270
323
|
|
|
271
324
|
interface ChatInputProperties {
|
|
325
|
+
branch?: ChatHierarchyBranch;
|
|
272
326
|
onCancel: (requestModel: ChatRequestModel) => void;
|
|
273
327
|
onQuery: (query: string) => void;
|
|
274
328
|
onUnpin: () => void;
|
|
275
329
|
onDragOver: (event: React.DragEvent) => void;
|
|
276
330
|
onDrop: (event: React.DragEvent) => void;
|
|
331
|
+
onPaste: (event: ClipboardEvent) => void;
|
|
277
332
|
onDeleteChangeSet: (sessionId: string) => void;
|
|
278
|
-
onDeleteChangeSetElement: (sessionId: string,
|
|
333
|
+
onDeleteChangeSetElement: (sessionId: string, uri: URI) => void;
|
|
279
334
|
onAddContextElement: () => void;
|
|
280
335
|
onDeleteContextElement: (index: number) => void;
|
|
281
336
|
onOpenContextElement: OpenContextElement;
|
|
@@ -284,8 +339,7 @@ interface ChatInputProperties {
|
|
|
284
339
|
chatModel: ChatModel;
|
|
285
340
|
pinnedAgent?: ChatAgent;
|
|
286
341
|
editorProvider: MonacoEditorProvider;
|
|
287
|
-
|
|
288
|
-
resourceUriProvider: () => URI;
|
|
342
|
+
uri: URI;
|
|
289
343
|
contextMenuCallback: (event: IMouseEvent) => void;
|
|
290
344
|
setEditorRef: (editor: SimpleMonacoEditor | undefined) => void;
|
|
291
345
|
showContext?: boolean;
|
|
@@ -297,37 +351,55 @@ interface ChatInputProperties {
|
|
|
297
351
|
decoratorService: ChangeSetDecoratorService;
|
|
298
352
|
initialValue?: string;
|
|
299
353
|
openerService: OpenerService;
|
|
300
|
-
suggestions: readonly ChatSuggestion[]
|
|
354
|
+
suggestions: readonly ChatSuggestion[];
|
|
355
|
+
currentRequest?: ChatRequestModel;
|
|
356
|
+
isEditing: boolean;
|
|
357
|
+
pending: boolean;
|
|
358
|
+
onResponseChanged: () => void;
|
|
301
359
|
}
|
|
302
360
|
|
|
303
361
|
const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInputProperties) => {
|
|
304
362
|
const onDeleteChangeSet = () => props.onDeleteChangeSet(props.chatModel.id);
|
|
305
|
-
const onDeleteChangeSetElement = (
|
|
363
|
+
const onDeleteChangeSetElement = (uri: URI) => props.onDeleteChangeSetElement(props.chatModel.id, uri);
|
|
306
364
|
|
|
307
|
-
const [inProgress, setInProgress] = React.useState(false);
|
|
308
365
|
const [isInputEmpty, setIsInputEmpty] = React.useState(true);
|
|
309
366
|
const [changeSetUI, setChangeSetUI] = React.useState(
|
|
310
|
-
() =>
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
)
|
|
319
|
-
: undefined
|
|
320
|
-
);
|
|
367
|
+
() => buildChangeSetUI(
|
|
368
|
+
props.chatModel.changeSet,
|
|
369
|
+
props.labelProvider,
|
|
370
|
+
props.decoratorService,
|
|
371
|
+
props.actionService.getActionsForChangeset(props.chatModel.changeSet),
|
|
372
|
+
onDeleteChangeSet,
|
|
373
|
+
onDeleteChangeSetElement
|
|
374
|
+
));
|
|
321
375
|
|
|
322
376
|
// eslint-disable-next-line no-null/no-null
|
|
323
377
|
const editorContainerRef = React.useRef<HTMLDivElement | null>(null);
|
|
324
378
|
// eslint-disable-next-line no-null/no-null
|
|
325
379
|
const placeholderRef = React.useRef<HTMLDivElement | null>(null);
|
|
326
380
|
const editorRef = React.useRef<SimpleMonacoEditor | undefined>(undefined);
|
|
381
|
+
// eslint-disable-next-line no-null/no-null
|
|
382
|
+
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
383
|
+
|
|
384
|
+
// Handle paste events on the container
|
|
385
|
+
const handlePaste = React.useCallback((event: ClipboardEvent) => {
|
|
386
|
+
props.onPaste(event);
|
|
387
|
+
}, [props.onPaste]);
|
|
388
|
+
|
|
389
|
+
// Set up paste handler on the container div
|
|
390
|
+
React.useEffect(() => {
|
|
391
|
+
const container = containerRef.current;
|
|
392
|
+
if (container) {
|
|
393
|
+
container.addEventListener('paste', handlePaste, true);
|
|
394
|
+
return () => {
|
|
395
|
+
container.removeEventListener('paste', handlePaste, true);
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
return undefined;
|
|
399
|
+
}, [handlePaste]);
|
|
327
400
|
|
|
328
401
|
React.useEffect(() => {
|
|
329
|
-
const uri = props.
|
|
330
|
-
const resource = props.resources.add(uri, '');
|
|
402
|
+
const uri = props.uri;
|
|
331
403
|
const createInputElement = async () => {
|
|
332
404
|
const paddingTop = 6;
|
|
333
405
|
const lineHeight = 20;
|
|
@@ -403,7 +475,6 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
403
475
|
createInputElement();
|
|
404
476
|
|
|
405
477
|
return () => {
|
|
406
|
-
resource.dispose();
|
|
407
478
|
props.setEditorRef(undefined);
|
|
408
479
|
if (editorRef.current) {
|
|
409
480
|
editorRef.current.dispose();
|
|
@@ -411,66 +482,42 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
411
482
|
};
|
|
412
483
|
}, []);
|
|
413
484
|
|
|
414
|
-
const responseListenerRef = React.useRef<Disposable>();
|
|
415
|
-
// track chat model updates to keep our UI in sync
|
|
416
|
-
// - keep "inProgress" in sync with the request state
|
|
417
|
-
// - keep "changeSetUI" in sync with the change set
|
|
418
485
|
React.useEffect(() => {
|
|
486
|
+
setChangeSetUI(buildChangeSetUI(
|
|
487
|
+
props.chatModel.changeSet,
|
|
488
|
+
props.labelProvider,
|
|
489
|
+
props.decoratorService,
|
|
490
|
+
props.actionService.getActionsForChangeset(props.chatModel.changeSet),
|
|
491
|
+
onDeleteChangeSet,
|
|
492
|
+
onDeleteChangeSetElement
|
|
493
|
+
));
|
|
419
494
|
const listener = props.chatModel.onDidChange(event => {
|
|
420
|
-
if (event
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
if (event.kind === 'removeChangeSet') {
|
|
430
|
-
setChangeSetUI(undefined);
|
|
431
|
-
} else if (event.kind === 'setChangeSet' || 'updateChangeSet') {
|
|
432
|
-
setChangeSetUI(buildChangeSetUI(
|
|
433
|
-
event.changeSet,
|
|
434
|
-
props.labelProvider,
|
|
435
|
-
props.decoratorService,
|
|
436
|
-
props.actionService.getActionsForChangeset(event.changeSet),
|
|
437
|
-
onDeleteChangeSet,
|
|
438
|
-
onDeleteChangeSetElement
|
|
439
|
-
));
|
|
440
|
-
}
|
|
495
|
+
if (ChatChangeEvent.isChangeSetEvent(event)) {
|
|
496
|
+
setChangeSetUI(buildChangeSetUI(
|
|
497
|
+
props.chatModel.changeSet,
|
|
498
|
+
props.labelProvider,
|
|
499
|
+
props.decoratorService,
|
|
500
|
+
props.actionService.getActionsForChangeset(props.chatModel.changeSet),
|
|
501
|
+
onDeleteChangeSet,
|
|
502
|
+
onDeleteChangeSetElement
|
|
503
|
+
));
|
|
441
504
|
}
|
|
442
505
|
});
|
|
443
|
-
setChangeSetUI(props.chatModel.changeSet
|
|
444
|
-
? buildChangeSetUI(
|
|
445
|
-
props.chatModel.changeSet,
|
|
446
|
-
props.labelProvider,
|
|
447
|
-
props.decoratorService,
|
|
448
|
-
props.actionService.getActionsForChangeset(props.chatModel.changeSet),
|
|
449
|
-
onDeleteChangeSet,
|
|
450
|
-
onDeleteChangeSetElement
|
|
451
|
-
)
|
|
452
|
-
: undefined);
|
|
453
506
|
return () => {
|
|
454
|
-
listener
|
|
455
|
-
responseListenerRef.current?.dispose();
|
|
456
|
-
responseListenerRef.current = undefined;
|
|
507
|
+
listener.dispose();
|
|
457
508
|
};
|
|
458
|
-
}, [props.chatModel]);
|
|
509
|
+
}, [props.chatModel, props.labelProvider, props.decoratorService, props.actionService]);
|
|
459
510
|
|
|
460
511
|
React.useEffect(() => {
|
|
461
512
|
const disposable = props.actionService.onDidChange(() => {
|
|
462
|
-
if (!props.chatModel.changeSet) { return; }
|
|
463
513
|
const newActions = props.actionService.getActionsForChangeset(props.chatModel.changeSet);
|
|
464
514
|
setChangeSetUI(current => !current ? current : { ...current, actions: newActions });
|
|
465
515
|
});
|
|
466
516
|
return () => disposable.dispose();
|
|
467
|
-
});
|
|
517
|
+
}, [props.actionService, props.chatModel.changeSet]);
|
|
468
518
|
|
|
469
519
|
React.useEffect(() => {
|
|
470
520
|
const disposable = props.decoratorService.onDidChangeDecorations(() => {
|
|
471
|
-
if (!props.chatModel.changeSet) {
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
521
|
setChangeSetUI(buildChangeSetUI(
|
|
475
522
|
props.chatModel.changeSet,
|
|
476
523
|
props.labelProvider,
|
|
@@ -493,9 +540,13 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
493
540
|
if (!value || value.trim().length === 0) {
|
|
494
541
|
return;
|
|
495
542
|
}
|
|
496
|
-
|
|
543
|
+
|
|
497
544
|
props.onQuery(value);
|
|
498
545
|
setValue('');
|
|
546
|
+
|
|
547
|
+
if (editorRef.current) {
|
|
548
|
+
editorRef.current.document.textEditorModel.setValue('');
|
|
549
|
+
}
|
|
499
550
|
}, [props.context, props.onQuery, setValue]);
|
|
500
551
|
|
|
501
552
|
const onKeyDown = React.useCallback((event: React.KeyboardEvent) => {
|
|
@@ -571,19 +622,38 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
571
622
|
: []),
|
|
572
623
|
] as Option[];
|
|
573
624
|
|
|
574
|
-
|
|
575
|
-
|
|
625
|
+
let rightOptions: Option[] = [];
|
|
626
|
+
const { currentRequest: latestRequest, isEditing, pending, onResponseChanged } = props;
|
|
627
|
+
React.useEffect(() => {
|
|
628
|
+
if (!latestRequest) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const disposable = latestRequest.response.onDidChange(onResponseChanged);
|
|
632
|
+
return () => disposable.dispose();
|
|
633
|
+
}, [latestRequest, onResponseChanged]);
|
|
634
|
+
if (isEditing) {
|
|
635
|
+
rightOptions = [{
|
|
636
|
+
title: nls.localize('theia/ai/chat-ui/send', 'Send (Enter)'),
|
|
637
|
+
handler: () => {
|
|
638
|
+
if (props.isEnabled) {
|
|
639
|
+
submit(editorRef.current?.document.textEditorModel.getValue() || '');
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
className: 'codicon-send',
|
|
643
|
+
disabled: isInputEmpty || !props.isEnabled
|
|
644
|
+
}];
|
|
645
|
+
} else if (pending) {
|
|
646
|
+
rightOptions = [{
|
|
576
647
|
title: nls.localize('theia/ai/chat-ui/cancel', 'Cancel (Esc)'),
|
|
577
648
|
handler: () => {
|
|
578
|
-
const latestRequest = getLatestRequest(props.chatModel);
|
|
579
649
|
if (latestRequest) {
|
|
580
650
|
props.onCancel(latestRequest);
|
|
581
651
|
}
|
|
582
|
-
setInProgress(false);
|
|
583
652
|
},
|
|
584
653
|
className: 'codicon-stop-circle'
|
|
585
|
-
}]
|
|
586
|
-
|
|
654
|
+
}];
|
|
655
|
+
} else {
|
|
656
|
+
rightOptions = [{
|
|
587
657
|
title: nls.localize('theia/ai/chat-ui/send', 'Send (Enter)'),
|
|
588
658
|
handler: () => {
|
|
589
659
|
if (props.isEnabled) {
|
|
@@ -593,24 +663,27 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
593
663
|
className: 'codicon-send',
|
|
594
664
|
disabled: isInputEmpty || !props.isEnabled
|
|
595
665
|
}];
|
|
666
|
+
}
|
|
596
667
|
|
|
597
668
|
const contextUI = buildContextUI(props.context, props.labelProvider, props.onDeleteContextElement, props.onOpenContextElement);
|
|
598
669
|
|
|
599
|
-
return
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
<div className='theia-ChatInput-Editor-Box'>
|
|
605
|
-
<div className='theia-ChatInput-Editor' ref={editorContainerRef} onKeyDown={onKeyDown} onFocus={handleInputFocus} onBlur={handleInputBlur}>
|
|
606
|
-
<div ref={placeholderRef} className='theia-ChatInput-Editor-Placeholder'>{nls.localizeByDefault('Ask a question')}</div>
|
|
607
|
-
</div>
|
|
608
|
-
{props.context && props.context.length > 0 &&
|
|
609
|
-
<ChatContext context={contextUI.context} />
|
|
670
|
+
return (
|
|
671
|
+
<div className='theia-ChatInput' onDragOver={props.onDragOver} onDrop={props.onDrop} ref={containerRef}>
|
|
672
|
+
{props.showSuggestions !== false && <ChatInputAgentSuggestions suggestions={props.suggestions} opener={props.openerService} />}
|
|
673
|
+
{props.showChangeSet && changeSetUI?.elements &&
|
|
674
|
+
<ChangeSetBox changeSet={changeSetUI} />
|
|
610
675
|
}
|
|
611
|
-
<
|
|
676
|
+
<div className='theia-ChatInput-Editor-Box'>
|
|
677
|
+
<div className='theia-ChatInput-Editor' ref={editorContainerRef} onKeyDown={onKeyDown} onFocus={handleInputFocus} onBlur={handleInputBlur}>
|
|
678
|
+
<div ref={placeholderRef} className='theia-ChatInput-Editor-Placeholder'>{nls.localizeByDefault('Ask a question')}</div>
|
|
679
|
+
</div>
|
|
680
|
+
{props.context && props.context.length > 0 &&
|
|
681
|
+
<ChatContext context={contextUI.context} />
|
|
682
|
+
}
|
|
683
|
+
<ChatInputOptions leftOptions={leftOptions} rightOptions={rightOptions} />
|
|
684
|
+
</div>
|
|
612
685
|
</div>
|
|
613
|
-
|
|
686
|
+
);
|
|
614
687
|
};
|
|
615
688
|
|
|
616
689
|
const noPropagation = (handler: () => void) => (e: React.MouseEvent) => {
|
|
@@ -624,28 +697,21 @@ const buildChangeSetUI = (
|
|
|
624
697
|
decoratorService: ChangeSetDecoratorService,
|
|
625
698
|
actions: ChangeSetActionRenderer[],
|
|
626
699
|
onDeleteChangeSet: () => void,
|
|
627
|
-
onDeleteChangeSetElement: (
|
|
628
|
-
): ChangeSetUI =>
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
additionalInfoSuffixIcon: decoratorService.getAdditionalInfoSuffixIcon(element),
|
|
639
|
-
openChange: element?.openChange?.bind(element),
|
|
640
|
-
apply: element.state !== 'applied' ? element?.apply?.bind(element) : undefined,
|
|
641
|
-
revert: element.state === 'applied' || element.state === 'stale' ? element?.revert?.bind(element) : undefined,
|
|
642
|
-
delete: () => onDeleteChangeSetElement(changeSet.getElements().indexOf(element))
|
|
643
|
-
} satisfies ChangeSetUIElement)),
|
|
644
|
-
actions
|
|
645
|
-
});
|
|
700
|
+
onDeleteChangeSetElement: (uri: URI) => void
|
|
701
|
+
): ChangeSetUI | undefined => {
|
|
702
|
+
const elements = changeSet.getElements();
|
|
703
|
+
return elements.length ? ({
|
|
704
|
+
title: changeSet.title,
|
|
705
|
+
changeSet,
|
|
706
|
+
deleteChangeSet: onDeleteChangeSet,
|
|
707
|
+
elements: changeSet.getElements().map(element => toUiElement(element, onDeleteChangeSetElement, labelProvider, decoratorService)),
|
|
708
|
+
actions
|
|
709
|
+
}) : undefined;
|
|
710
|
+
};
|
|
646
711
|
|
|
647
712
|
interface ChangeSetUIElement {
|
|
648
713
|
name: string;
|
|
714
|
+
uri: string;
|
|
649
715
|
iconClass: string;
|
|
650
716
|
nameClass: string;
|
|
651
717
|
additionalInfo: string;
|
|
@@ -677,48 +743,70 @@ const ChangeSetBox: React.FunctionComponent<{ changeSet: ChangeSetUI }> = React.
|
|
|
677
743
|
</div>
|
|
678
744
|
<div className='theia-ChatInput-ChangeSet-List'>
|
|
679
745
|
<ul>
|
|
680
|
-
{elements.map(
|
|
681
|
-
<li key={index} title={nls.localize('theia/ai/chat-ui/openDiff', 'Open Diff')} onClick={() => element.openChange?.()}>
|
|
682
|
-
<div className={`theia-ChatInput-ChangeSet-Icon ${element.iconClass}`}>
|
|
683
|
-
</div>
|
|
684
|
-
<div className='theia-ChatInput-ChangeSet-labelParts'>
|
|
685
|
-
<span className={`theia-ChatInput-ChangeSet-title ${element.nameClass}`}>
|
|
686
|
-
{element.name}
|
|
687
|
-
</span>
|
|
688
|
-
<div className='theia-ChatInput-ChangeSet-additionalInfo'>
|
|
689
|
-
{element.additionalInfo && <span>{element.additionalInfo}</span>}
|
|
690
|
-
{element.additionalInfoSuffixIcon
|
|
691
|
-
&& <div className={`theia-ChatInput-ChangeSet-AdditionalInfo-SuffixIcon ${element.additionalInfoSuffixIcon.join(' ')}`}></div>}
|
|
692
|
-
</div>
|
|
693
|
-
</div>
|
|
694
|
-
<div className='theia-ChatInput-ChangeSet-Actions'>
|
|
695
|
-
{element.open && (
|
|
696
|
-
<span
|
|
697
|
-
className='codicon codicon-file action'
|
|
698
|
-
title={nls.localize('theia/ai/chat-ui/openOriginalFile', 'Open Original File')}
|
|
699
|
-
onClick={noPropagation(() => element.open!())}
|
|
700
|
-
/>)}
|
|
701
|
-
{element.revert && (
|
|
702
|
-
<span
|
|
703
|
-
className='codicon codicon-discard action'
|
|
704
|
-
title={nls.localizeByDefault('Revert')}
|
|
705
|
-
onClick={noPropagation(() => element.revert!())}
|
|
706
|
-
/>)}
|
|
707
|
-
{element.apply && (
|
|
708
|
-
<span
|
|
709
|
-
className='codicon codicon-check action'
|
|
710
|
-
title={nls.localizeByDefault('Apply')}
|
|
711
|
-
onClick={noPropagation(() => element.apply!())}
|
|
712
|
-
/>)}
|
|
713
|
-
<span className='codicon codicon-close action' title={nls.localizeByDefault('Delete')} onClick={noPropagation(() => element.delete())} />
|
|
714
|
-
</div>
|
|
715
|
-
</li>
|
|
716
|
-
))}
|
|
746
|
+
{elements.map(element => ChangeSetElement(element))}
|
|
717
747
|
</ul>
|
|
718
748
|
</div>
|
|
719
749
|
</div>
|
|
720
750
|
));
|
|
721
751
|
|
|
752
|
+
function toUiElement(element: ChangeSetElement,
|
|
753
|
+
onDeleteChangeSetElement: (uri: URI) => void,
|
|
754
|
+
labelProvider: LabelProvider,
|
|
755
|
+
decoratorService: ChangeSetDecoratorService
|
|
756
|
+
): ChangeSetUIElement {
|
|
757
|
+
return ({
|
|
758
|
+
open: element.open?.bind(element),
|
|
759
|
+
uri: element.uri.toString(),
|
|
760
|
+
iconClass: element.icon ?? labelProvider.getIcon(element.uri) ?? labelProvider.fileIcon,
|
|
761
|
+
nameClass: `${element.type} ${element.state}`,
|
|
762
|
+
name: element.name ?? labelProvider.getName(element.uri),
|
|
763
|
+
additionalInfo: element.additionalInfo ?? labelProvider.getDetails(element.uri),
|
|
764
|
+
additionalInfoSuffixIcon: decoratorService.getAdditionalInfoSuffixIcon(element),
|
|
765
|
+
openChange: element?.openChange?.bind(element),
|
|
766
|
+
apply: element.state !== 'applied' ? element?.apply?.bind(element) : undefined,
|
|
767
|
+
revert: element.state === 'applied' || element.state === 'stale' ? element?.revert?.bind(element) : undefined,
|
|
768
|
+
delete: () => onDeleteChangeSetElement(element.uri)
|
|
769
|
+
} satisfies ChangeSetUIElement);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const ChangeSetElement: React.FC<ChangeSetUIElement> = element => (
|
|
773
|
+
<li key={element.uri} title={nls.localize('theia/ai/chat-ui/openDiff', 'Open Diff')} onClick={() => element.openChange?.()}>
|
|
774
|
+
<div className={`theia-ChatInput-ChangeSet-Icon ${element.iconClass}`}>
|
|
775
|
+
</div>
|
|
776
|
+
<div className='theia-ChatInput-ChangeSet-labelParts'>
|
|
777
|
+
<span className={`theia-ChatInput-ChangeSet-title ${element.nameClass}`}>
|
|
778
|
+
{element.name}
|
|
779
|
+
</span>
|
|
780
|
+
<div className='theia-ChatInput-ChangeSet-additionalInfo'>
|
|
781
|
+
{element.additionalInfo && <span>{element.additionalInfo}</span>}
|
|
782
|
+
{element.additionalInfoSuffixIcon
|
|
783
|
+
&& <div className={`theia-ChatInput-ChangeSet-AdditionalInfo-SuffixIcon ${element.additionalInfoSuffixIcon.join(' ')}`}></div>}
|
|
784
|
+
</div>
|
|
785
|
+
</div>
|
|
786
|
+
<div className='theia-ChatInput-ChangeSet-Actions'>
|
|
787
|
+
{element.open && (
|
|
788
|
+
<span
|
|
789
|
+
className='codicon codicon-file action'
|
|
790
|
+
title={nls.localize('theia/ai/chat-ui/openOriginalFile', 'Open Original File')}
|
|
791
|
+
onClick={noPropagation(() => element.open!())}
|
|
792
|
+
/>)}
|
|
793
|
+
{element.revert && (
|
|
794
|
+
<span
|
|
795
|
+
className='codicon codicon-discard action'
|
|
796
|
+
title={nls.localizeByDefault('Revert')}
|
|
797
|
+
onClick={noPropagation(() => element.revert!())}
|
|
798
|
+
/>)}
|
|
799
|
+
{element.apply && (
|
|
800
|
+
<span
|
|
801
|
+
className='codicon codicon-check action'
|
|
802
|
+
title={nls.localizeByDefault('Apply')}
|
|
803
|
+
onClick={noPropagation(() => element.apply!())}
|
|
804
|
+
/>)}
|
|
805
|
+
<span className='codicon codicon-close action' title={nls.localizeByDefault('Delete')} onClick={noPropagation(() => element.delete())} />
|
|
806
|
+
</div>
|
|
807
|
+
</li>
|
|
808
|
+
);
|
|
809
|
+
|
|
722
810
|
interface ChatInputOptionsProps {
|
|
723
811
|
leftOptions: Option[];
|
|
724
812
|
rightOptions: Option[];
|
|
@@ -766,11 +854,6 @@ const ChatInputOptions: React.FunctionComponent<ChatInputOptionsProps> = ({ left
|
|
|
766
854
|
</div>
|
|
767
855
|
);
|
|
768
856
|
|
|
769
|
-
function getLatestRequest(chatModel: ChatModel): ChatRequestModel | undefined {
|
|
770
|
-
const requests = chatModel.getRequests();
|
|
771
|
-
return requests.length > 0 ? requests[requests.length - 1] : undefined;
|
|
772
|
-
}
|
|
773
|
-
|
|
774
857
|
function buildContextUI(
|
|
775
858
|
context: readonly AIVariableResolutionRequest[] | undefined,
|
|
776
859
|
labelProvider: LabelProvider,
|
|
@@ -782,6 +865,7 @@ function buildContextUI(
|
|
|
782
865
|
}
|
|
783
866
|
return {
|
|
784
867
|
context: context.map((element, index) => ({
|
|
868
|
+
variable: element,
|
|
785
869
|
name: labelProvider.getName(element),
|
|
786
870
|
iconClass: labelProvider.getIcon(element),
|
|
787
871
|
nameClass: element.variable.name,
|
|
@@ -795,6 +879,7 @@ function buildContextUI(
|
|
|
795
879
|
|
|
796
880
|
interface ChatContextUI {
|
|
797
881
|
context: {
|
|
882
|
+
variable: AIVariableResolutionRequest,
|
|
798
883
|
name: string;
|
|
799
884
|
iconClass: string;
|
|
800
885
|
nameClass: string;
|
|
@@ -808,20 +893,45 @@ interface ChatContextUI {
|
|
|
808
893
|
const ChatContext: React.FunctionComponent<ChatContextUI> = ({ context }) => (
|
|
809
894
|
<div className="theia-ChatInput-ChatContext">
|
|
810
895
|
<ul>
|
|
811
|
-
{context.map((element, index) =>
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
<
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
896
|
+
{context.map((element, index) => {
|
|
897
|
+
if (ImageContextVariable.isImageContextRequest(element.variable)) {
|
|
898
|
+
const variable = ImageContextVariable.parseRequest(element.variable)!;
|
|
899
|
+
return <li key={index} className="theia-ChatInput-ChatContext-Element theia-ChatInput-ImageContext-Element"
|
|
900
|
+
title={variable.name ?? variable.wsRelativePath} onClick={() => element.open?.()}>
|
|
901
|
+
<div className="theia-ChatInput-ChatContext-Row">
|
|
902
|
+
<div className={`theia-ChatInput-ChatContext-Icon ${element.iconClass}`} />
|
|
903
|
+
<div className="theia-ChatInput-ChatContext-labelParts">
|
|
904
|
+
<span className={`theia-ChatInput-ChatContext-title ${element.nameClass}`}>
|
|
905
|
+
{variable.name ?? variable.wsRelativePath?.split('/').pop()}
|
|
906
|
+
</span>
|
|
907
|
+
<span className='theia-ChatInput-ChatContext-additionalInfo'>
|
|
908
|
+
{element.additionalInfo}
|
|
909
|
+
</span>
|
|
910
|
+
</div>
|
|
911
|
+
<span className="codicon codicon-close action" title={nls.localizeByDefault('Delete')} onClick={e => { e.stopPropagation(); element.delete(); }} />
|
|
912
|
+
</div>
|
|
913
|
+
<div className="theia-ChatInput-ChatContext-ImageRow">
|
|
914
|
+
<div className='theia-ChatInput-ImagePreview-Item'>
|
|
915
|
+
<img src={`data:${variable.mimeType};base64,${variable.data}`} alt={variable.name} />
|
|
916
|
+
</div>
|
|
917
|
+
</div>
|
|
918
|
+
</li>;
|
|
919
|
+
}
|
|
920
|
+
return <li key={index} className="theia-ChatInput-ChatContext-Element" title={element.details} onClick={() => element.open?.()}>
|
|
921
|
+
<div className="theia-ChatInput-ChatContext-Row">
|
|
922
|
+
<div className={`theia-ChatInput-ChatContext-Icon ${element.iconClass}`} />
|
|
923
|
+
<div className="theia-ChatInput-ChatContext-labelParts">
|
|
924
|
+
<span className={`theia-ChatInput-ChatContext-title ${element.nameClass}`}>
|
|
925
|
+
{element.name}
|
|
926
|
+
</span>
|
|
927
|
+
<span className='theia-ChatInput-ChatContext-additionalInfo'>
|
|
928
|
+
{element.additionalInfo}
|
|
929
|
+
</span>
|
|
930
|
+
</div>
|
|
931
|
+
<span className="codicon codicon-close action" title={nls.localizeByDefault('Delete')} onClick={e => { e.stopPropagation(); element.delete(); }} />
|
|
821
932
|
</div>
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
))}
|
|
933
|
+
</li>;
|
|
934
|
+
})}
|
|
825
935
|
</ul>
|
|
826
936
|
</div>
|
|
827
937
|
);
|