@theia/ai-chat-ui 1.63.0-next.52 → 1.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/ai-chat-ui-contribution.d.ts +2 -0
- package/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -1
- package/lib/browser/ai-chat-ui-contribution.js +37 -9
- package/lib/browser/ai-chat-ui-contribution.js.map +1 -1
- package/lib/browser/chat-input-widget.d.ts +18 -11
- package/lib/browser/chat-input-widget.d.ts.map +1 -1
- package/lib/browser/chat-input-widget.js +121 -18
- package/lib/browser/chat-input-widget.js.map +1 -1
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts +4 -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 +58 -25
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
- package/lib/browser/chat-view-contribution.d.ts +2 -0
- package/lib/browser/chat-view-contribution.d.ts.map +1 -1
- package/lib/browser/chat-view-contribution.js +29 -17
- package/lib/browser/chat-view-contribution.js.map +1 -1
- package/lib/browser/chat-view-widget-toolbar-contribution.d.ts +2 -0
- package/lib/browser/chat-view-widget-toolbar-contribution.d.ts.map +1 -1
- package/lib/browser/chat-view-widget-toolbar-contribution.js +13 -5
- package/lib/browser/chat-view-widget-toolbar-contribution.js.map +1 -1
- package/package.json +11 -11
- package/src/browser/ai-chat-ui-contribution.ts +28 -10
- package/src/browser/chat-input-widget.tsx +148 -29
- package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +86 -51
- package/src/browser/chat-view-contribution.ts +28 -17
- package/src/browser/chat-view-widget-toolbar-contribution.tsx +12 -5
- package/src/browser/style/index.css +34 -5
|
@@ -20,19 +20,21 @@ import {
|
|
|
20
20
|
import { ChangeSetDecoratorService } from '@theia/ai-chat/lib/browser/change-set-decorator-service';
|
|
21
21
|
import { ImageContextVariable } from '@theia/ai-chat/lib/common/image-context-variable';
|
|
22
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';
|
|
23
|
+
import { AgentCompletionNotificationService, FrontendVariableService, AIActivationService } from '@theia/ai-core/lib/browser';
|
|
24
|
+
import { DisposableCollection, Emitter, InMemoryResources, URI, nls } from '@theia/core';
|
|
25
25
|
import { ContextMenuRenderer, LabelProvider, Message, OpenerService, ReactWidget } from '@theia/core/lib/browser';
|
|
26
26
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
27
27
|
import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify';
|
|
28
28
|
import * as React from '@theia/core/shared/react';
|
|
29
|
-
import { IMouseEvent } from '@theia/monaco-editor-core';
|
|
29
|
+
import { IMouseEvent, Range } from '@theia/monaco-editor-core';
|
|
30
30
|
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
|
|
31
31
|
import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
|
|
32
32
|
import { ChangeSetActionRenderer, ChangeSetActionService } from './change-set-actions/change-set-action-service';
|
|
33
33
|
import { ChatInputAgentSuggestions } from './chat-input-agent-suggestions';
|
|
34
34
|
import { CHAT_VIEW_LANGUAGE_EXTENSION } from './chat-view-language-contribution';
|
|
35
35
|
import { ContextVariablePicker } from './context-variable-picker';
|
|
36
|
+
import { TASK_CONTEXT_VARIABLE } from '@theia/ai-chat/lib/browser/task-context-variable';
|
|
37
|
+
import { IModelDeltaDecoration } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
|
|
36
38
|
|
|
37
39
|
type Query = (query: string) => Promise<void>;
|
|
38
40
|
type Unpin = () => void;
|
|
@@ -78,6 +80,9 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
78
80
|
@inject(ChangeSetActionService)
|
|
79
81
|
protected readonly changeSetActionService: ChangeSetActionService;
|
|
80
82
|
|
|
83
|
+
@inject(AgentCompletionNotificationService)
|
|
84
|
+
protected readonly agentNotificationService: AgentCompletionNotificationService;
|
|
85
|
+
|
|
81
86
|
@inject(ChangeSetDecoratorService)
|
|
82
87
|
protected readonly changeSetDecoratorService: ChangeSetDecoratorService;
|
|
83
88
|
|
|
@@ -87,12 +92,16 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
87
92
|
@inject(ChatService)
|
|
88
93
|
protected readonly chatService: ChatService;
|
|
89
94
|
|
|
95
|
+
@inject(AIActivationService)
|
|
96
|
+
protected readonly aiActivationService: AIActivationService;
|
|
97
|
+
|
|
90
98
|
protected editorRef: SimpleMonacoEditor | undefined = undefined;
|
|
91
99
|
protected readonly editorReady = new Deferred<void>();
|
|
92
100
|
|
|
93
101
|
protected isEnabled = false;
|
|
102
|
+
protected heightInLines = 12;
|
|
94
103
|
|
|
95
|
-
|
|
104
|
+
protected _branch?: ChatHierarchyBranch;
|
|
96
105
|
set branch(branch: ChatHierarchyBranch | undefined) {
|
|
97
106
|
if (this._branch !== branch) {
|
|
98
107
|
this._branch = branch;
|
|
@@ -100,34 +109,34 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
100
109
|
}
|
|
101
110
|
}
|
|
102
111
|
|
|
103
|
-
|
|
112
|
+
protected _onQuery: Query;
|
|
104
113
|
set onQuery(query: Query) {
|
|
105
114
|
this._onQuery = query;
|
|
106
115
|
}
|
|
107
|
-
|
|
116
|
+
protected _onUnpin: Unpin;
|
|
108
117
|
set onUnpin(unpin: Unpin) {
|
|
109
118
|
this._onUnpin = unpin;
|
|
110
119
|
}
|
|
111
|
-
|
|
120
|
+
protected _onCancel: Cancel;
|
|
112
121
|
set onCancel(cancel: Cancel) {
|
|
113
122
|
this._onCancel = cancel;
|
|
114
123
|
}
|
|
115
|
-
|
|
124
|
+
protected _onDeleteChangeSet: DeleteChangeSet;
|
|
116
125
|
set onDeleteChangeSet(deleteChangeSet: DeleteChangeSet) {
|
|
117
126
|
this._onDeleteChangeSet = deleteChangeSet;
|
|
118
127
|
}
|
|
119
|
-
|
|
128
|
+
protected _onDeleteChangeSetElement: DeleteChangeSetElement;
|
|
120
129
|
set onDeleteChangeSetElement(deleteChangeSetElement: DeleteChangeSetElement) {
|
|
121
130
|
this._onDeleteChangeSetElement = deleteChangeSetElement;
|
|
122
131
|
}
|
|
123
132
|
|
|
124
|
-
|
|
133
|
+
protected _initialValue?: string;
|
|
125
134
|
set initialValue(value: string | undefined) {
|
|
126
135
|
this._initialValue = value;
|
|
127
136
|
}
|
|
128
137
|
|
|
129
138
|
protected onDisposeForChatModel = new DisposableCollection();
|
|
130
|
-
|
|
139
|
+
protected _chatModel: ChatModel;
|
|
131
140
|
set chatModel(chatModel: ChatModel) {
|
|
132
141
|
this.onDisposeForChatModel.dispose();
|
|
133
142
|
this.onDisposeForChatModel = new DisposableCollection();
|
|
@@ -139,17 +148,25 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
139
148
|
this._chatModel = chatModel;
|
|
140
149
|
this.update();
|
|
141
150
|
}
|
|
142
|
-
|
|
151
|
+
protected _pinnedAgent: ChatAgent | undefined;
|
|
143
152
|
set pinnedAgent(pinnedAgent: ChatAgent | undefined) {
|
|
144
153
|
this._pinnedAgent = pinnedAgent;
|
|
145
154
|
this.update();
|
|
146
155
|
}
|
|
147
156
|
|
|
157
|
+
protected onDidResizeEmitter = new Emitter<void>();
|
|
158
|
+
readonly onDidResize = this.onDidResizeEmitter.event;
|
|
159
|
+
|
|
148
160
|
@postConstruct()
|
|
149
161
|
protected init(): void {
|
|
150
162
|
this.id = AIChatInputWidget.ID;
|
|
151
163
|
this.title.closable = false;
|
|
152
164
|
this.toDispose.push(this.resources.add(this.getResourceUri(), ''));
|
|
165
|
+
this.toDispose.push(this.aiActivationService.onDidChangeActiveStatus(() => {
|
|
166
|
+
this.setEnabled(this.aiActivationService.isActive);
|
|
167
|
+
}));
|
|
168
|
+
this.toDispose.push(this.onDidResizeEmitter);
|
|
169
|
+
this.setEnabled(this.aiActivationService.isActive);
|
|
153
170
|
this.update();
|
|
154
171
|
}
|
|
155
172
|
|
|
@@ -162,6 +179,18 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
162
179
|
});
|
|
163
180
|
}
|
|
164
181
|
|
|
182
|
+
protected async handleAgentCompletion(request: ChatRequestModel): Promise<void> {
|
|
183
|
+
try {
|
|
184
|
+
const agentId = request.agentId;
|
|
185
|
+
|
|
186
|
+
if (agentId) {
|
|
187
|
+
await this.agentNotificationService.showCompletionNotification(agentId);
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error('Failed to handle agent completion notification:', error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
165
194
|
protected getResourceUri(): URI {
|
|
166
195
|
return new URI(`ai-chat:/input.${CHAT_VIEW_LANGUAGE_EXTENSION}`);
|
|
167
196
|
}
|
|
@@ -187,12 +216,14 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
187
216
|
onDragOver={this.onDragOver.bind(this)}
|
|
188
217
|
onDrop={this.onDrop.bind(this)}
|
|
189
218
|
onPaste={this.onPaste.bind(this)}
|
|
219
|
+
onEscape={this.onEscape.bind(this)}
|
|
190
220
|
onDeleteChangeSet={this._onDeleteChangeSet.bind(this)}
|
|
191
221
|
onDeleteChangeSetElement={this._onDeleteChangeSetElement.bind(this)}
|
|
192
222
|
onAddContextElement={this.addContextElement.bind(this)}
|
|
193
223
|
onDeleteContextElement={this.deleteContextElement.bind(this)}
|
|
194
224
|
onOpenContextElement={this.openContextElement.bind(this)}
|
|
195
225
|
context={this.getContext()}
|
|
226
|
+
onAgentCompletion={this.handleAgentCompletion.bind(this)}
|
|
196
227
|
chatModel={this._chatModel}
|
|
197
228
|
pinnedAgent={this._pinnedAgent}
|
|
198
229
|
editorProvider={this.editorProvider}
|
|
@@ -216,11 +247,13 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
216
247
|
currentRequest={currentRequest}
|
|
217
248
|
isEditing={isEditing}
|
|
218
249
|
pending={pending}
|
|
250
|
+
heightInLines={this.heightInLines}
|
|
219
251
|
onResponseChanged={() => {
|
|
220
252
|
if (isPending() !== pending) {
|
|
221
253
|
this.update();
|
|
222
254
|
}
|
|
223
255
|
}}
|
|
256
|
+
onResize={() => this.onDidResizeEmitter.fire()}
|
|
224
257
|
/>
|
|
225
258
|
);
|
|
226
259
|
}
|
|
@@ -279,6 +312,10 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
279
312
|
});
|
|
280
313
|
}
|
|
281
314
|
|
|
315
|
+
protected onEscape(): void {
|
|
316
|
+
// No op
|
|
317
|
+
}
|
|
318
|
+
|
|
282
319
|
protected async openContextElement(request: AIVariableResolutionRequest): Promise<void> {
|
|
283
320
|
const session = this.chatService.getSessions().find(candidate => candidate.model.id === this._chatModel.id);
|
|
284
321
|
const context = { session };
|
|
@@ -333,7 +370,9 @@ interface ChatInputProperties {
|
|
|
333
370
|
onDeleteChangeSetElement: (sessionId: string, uri: URI) => void;
|
|
334
371
|
onAddContextElement: () => void;
|
|
335
372
|
onDeleteContextElement: (index: number) => void;
|
|
373
|
+
onEscape: () => void;
|
|
336
374
|
onOpenContextElement: OpenContextElement;
|
|
375
|
+
onAgentCompletion: (request: ChatRequestModel) => void;
|
|
337
376
|
context?: readonly AIVariableResolutionRequest[];
|
|
338
377
|
isEnabled?: boolean;
|
|
339
378
|
chatModel: ChatModel;
|
|
@@ -355,9 +394,16 @@ interface ChatInputProperties {
|
|
|
355
394
|
currentRequest?: ChatRequestModel;
|
|
356
395
|
isEditing: boolean;
|
|
357
396
|
pending: boolean;
|
|
397
|
+
heightInLines?: number;
|
|
358
398
|
onResponseChanged: () => void;
|
|
399
|
+
onResize: () => void;
|
|
359
400
|
}
|
|
360
401
|
|
|
402
|
+
// Utility to check if we have task context in the chat model
|
|
403
|
+
const hasTaskContext = (chatModel: ChatModel): boolean => chatModel.context.getVariables().some(variable =>
|
|
404
|
+
variable.variable?.id === TASK_CONTEXT_VARIABLE.id
|
|
405
|
+
);
|
|
406
|
+
|
|
361
407
|
const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInputProperties) => {
|
|
362
408
|
const onDeleteChangeSet = () => props.onDeleteChangeSet(props.chatModel.id);
|
|
363
409
|
const onDeleteChangeSetElement = (uri: URI) => props.onDeleteChangeSetElement(props.chatModel.id, uri);
|
|
@@ -381,6 +427,17 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
381
427
|
// eslint-disable-next-line no-null/no-null
|
|
382
428
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
383
429
|
|
|
430
|
+
// On the first request of the chat, if the chat has a task context and a pinned
|
|
431
|
+
// agent, show a "Perform this task." placeholder which is the message to send by default
|
|
432
|
+
const isFirstRequest = props.chatModel.getRequests().length === 0;
|
|
433
|
+
const shouldUseTaskPlaceholder = isFirstRequest && props.pinnedAgent && hasTaskContext(props.chatModel);
|
|
434
|
+
const taskPlaceholder = nls.localize('theia/ai/chat-ui/performThisTask', 'Perform this task.');
|
|
435
|
+
const placeholderText = !props.isEnabled
|
|
436
|
+
? nls.localize('theia/ai/chat-ui/aiDisabled', 'AI features are disabled')
|
|
437
|
+
: shouldUseTaskPlaceholder
|
|
438
|
+
? taskPlaceholder
|
|
439
|
+
: nls.localizeByDefault('Ask a question');
|
|
440
|
+
|
|
384
441
|
// Handle paste events on the container
|
|
385
442
|
const handlePaste = React.useCallback((event: ClipboardEvent) => {
|
|
386
443
|
props.onPaste(event);
|
|
@@ -403,7 +460,8 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
403
460
|
const createInputElement = async () => {
|
|
404
461
|
const paddingTop = 6;
|
|
405
462
|
const lineHeight = 20;
|
|
406
|
-
const
|
|
463
|
+
const maxHeightPx = (props.heightInLines ?? 12) * lineHeight;
|
|
464
|
+
|
|
407
465
|
const editor = await props.editorProvider.createSimpleInline(uri, editorContainerRef.current!, {
|
|
408
466
|
language: CHAT_VIEW_LANGUAGE_EXTENSION,
|
|
409
467
|
// Disable code lens, inlay hints and hover support to avoid console errors from other contributions
|
|
@@ -438,12 +496,17 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
438
496
|
if (editorContainerRef.current) {
|
|
439
497
|
editorContainerRef.current.style.overflowY = 'auto'; // ensure vertical scrollbar
|
|
440
498
|
editorContainerRef.current.style.height = (lineHeight + (2 * paddingTop)) + 'px';
|
|
499
|
+
|
|
500
|
+
editorContainerRef.current.addEventListener('wheel', e => {
|
|
501
|
+
// Prevent parent from scrolling
|
|
502
|
+
e.stopPropagation();
|
|
503
|
+
}, { passive: false });
|
|
441
504
|
}
|
|
442
505
|
|
|
443
506
|
const updateEditorHeight = () => {
|
|
444
507
|
if (editorContainerRef.current) {
|
|
445
508
|
const contentHeight = editor.getControl().getContentHeight() + paddingTop;
|
|
446
|
-
editorContainerRef.current.style.height = `${Math.min(contentHeight,
|
|
509
|
+
editorContainerRef.current.style.height = `${Math.min(contentHeight, maxHeightPx)}px`;
|
|
447
510
|
}
|
|
448
511
|
};
|
|
449
512
|
|
|
@@ -453,7 +516,10 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
453
516
|
updateEditorHeight();
|
|
454
517
|
handleOnChange();
|
|
455
518
|
});
|
|
456
|
-
const resizeObserver = new ResizeObserver(
|
|
519
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
520
|
+
updateEditorHeight();
|
|
521
|
+
props.onResize();
|
|
522
|
+
});
|
|
457
523
|
if (editorContainerRef.current) {
|
|
458
524
|
resizeObserver.observe(editorContainerRef.current);
|
|
459
525
|
}
|
|
@@ -465,12 +531,46 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
465
531
|
props.contextMenuCallback(e.event)
|
|
466
532
|
);
|
|
467
533
|
|
|
534
|
+
const updateLineCounts = () => {
|
|
535
|
+
// We need the line numbers to allow scrolling by using the keyboard
|
|
536
|
+
const model = editor.getControl().getModel()!;
|
|
537
|
+
const lineCount = model.getLineCount();
|
|
538
|
+
const decorations: IModelDeltaDecoration[] = [];
|
|
539
|
+
|
|
540
|
+
for (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) {
|
|
541
|
+
decorations.push({
|
|
542
|
+
range: new Range(lineNumber, 1, lineNumber, 1),
|
|
543
|
+
options: {
|
|
544
|
+
description: `line-number-${lineNumber}`,
|
|
545
|
+
isWholeLine: false,
|
|
546
|
+
className: `line-number-${lineNumber}`,
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const lineNumbers = model.getAllDecorations().filter(predicate => predicate.options.description?.startsWith('line-number-'));
|
|
552
|
+
editor.getControl().removeDecorations(lineNumbers.map(d => d.id));
|
|
553
|
+
editor.getControl().createDecorationsCollection(decorations);
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
editor.getControl().getModel()?.onDidChangeContent(() => {
|
|
557
|
+
updateLineCounts();
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
editor.getControl().onDidChangeCursorPosition(e => {
|
|
561
|
+
const lineNumber = e.position.lineNumber;
|
|
562
|
+
const line = editor.getControl().getDomNode()?.querySelector(`.line-number-${lineNumber}`);
|
|
563
|
+
line?.scrollIntoView({ behavior: 'instant', block: 'nearest' });
|
|
564
|
+
});
|
|
565
|
+
|
|
468
566
|
editorRef.current = editor;
|
|
469
567
|
props.setEditorRef(editor);
|
|
470
568
|
|
|
471
569
|
if (props.initialValue) {
|
|
472
570
|
setValue(props.initialValue);
|
|
473
571
|
}
|
|
572
|
+
|
|
573
|
+
updateLineCounts();
|
|
474
574
|
};
|
|
475
575
|
createInputElement();
|
|
476
576
|
|
|
@@ -502,6 +602,15 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
502
602
|
onDeleteChangeSetElement
|
|
503
603
|
));
|
|
504
604
|
}
|
|
605
|
+
if (event.kind === 'addRequest') {
|
|
606
|
+
// Listen for when this request's response becomes complete
|
|
607
|
+
const responseListener = event.request.response.onDidChange(() => {
|
|
608
|
+
if (event.request.response.isComplete) {
|
|
609
|
+
props.onAgentCompletion(event.request);
|
|
610
|
+
responseListener.dispose(); // Clean up the listener once notification is sent
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
}
|
|
505
614
|
});
|
|
506
615
|
return () => {
|
|
507
616
|
listener.dispose();
|
|
@@ -536,18 +645,21 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
536
645
|
}
|
|
537
646
|
}, [editorRef]);
|
|
538
647
|
|
|
648
|
+
// Without user input, if we can default to "Perform this task.", do so
|
|
539
649
|
const submit = React.useCallback(function submit(value: string): void {
|
|
540
|
-
|
|
650
|
+
let effectiveValue = value;
|
|
651
|
+
if ((!value || value.trim().length === 0) && shouldUseTaskPlaceholder) {
|
|
652
|
+
effectiveValue = taskPlaceholder;
|
|
653
|
+
}
|
|
654
|
+
if (!effectiveValue || effectiveValue.trim().length === 0) {
|
|
541
655
|
return;
|
|
542
656
|
}
|
|
543
|
-
|
|
544
|
-
props.onQuery(value);
|
|
657
|
+
props.onQuery(effectiveValue);
|
|
545
658
|
setValue('');
|
|
546
|
-
|
|
547
|
-
if (editorRef.current) {
|
|
659
|
+
if (editorRef.current && !editorRef.current.document.textEditorModel.isDisposed()) {
|
|
548
660
|
editorRef.current.document.textEditorModel.setValue('');
|
|
549
661
|
}
|
|
550
|
-
}, [props.context, props.onQuery, setValue]);
|
|
662
|
+
}, [props.context, props.onQuery, setValue, shouldUseTaskPlaceholder, taskPlaceholder]);
|
|
551
663
|
|
|
552
664
|
const onKeyDown = React.useCallback((event: React.KeyboardEvent) => {
|
|
553
665
|
if (!props.isEnabled) {
|
|
@@ -555,7 +667,12 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
555
667
|
}
|
|
556
668
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
557
669
|
event.preventDefault();
|
|
558
|
-
submit(
|
|
670
|
+
// On Enter, read input and submit (handles task context)
|
|
671
|
+
const currentValue = editorRef.current?.document.textEditorModel.getValue() || '';
|
|
672
|
+
submit(currentValue);
|
|
673
|
+
} else if (event.key === 'Escape') {
|
|
674
|
+
event.preventDefault();
|
|
675
|
+
props.onEscape();
|
|
559
676
|
}
|
|
560
677
|
}, [props.isEnabled, submit]);
|
|
561
678
|
|
|
@@ -606,7 +723,8 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
606
723
|
? [{
|
|
607
724
|
title: nls.localize('theia/ai/chat-ui/attachToContext', 'Attach elements to context'),
|
|
608
725
|
handler: () => props.onAddContextElement(),
|
|
609
|
-
className: 'codicon-add'
|
|
726
|
+
className: 'codicon-add',
|
|
727
|
+
disabled: !props.isEnabled
|
|
610
728
|
}]
|
|
611
729
|
: []),
|
|
612
730
|
...(props.showPinnedAgent
|
|
@@ -614,6 +732,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
614
732
|
title: props.pinnedAgent ? nls.localize('theia/ai/chat-ui/unpinAgent', 'Unpin Agent') : nls.localize('theia/ai/chat-ui/pinAgent', 'Pin Agent'),
|
|
615
733
|
handler: props.pinnedAgent ? props.onUnpin : handlePin,
|
|
616
734
|
className: 'at-icon',
|
|
735
|
+
disabled: !props.isEnabled,
|
|
617
736
|
text: {
|
|
618
737
|
align: 'right',
|
|
619
738
|
content: props.pinnedAgent && props.pinnedAgent.name
|
|
@@ -640,7 +759,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
640
759
|
}
|
|
641
760
|
},
|
|
642
761
|
className: 'codicon-send',
|
|
643
|
-
disabled: isInputEmpty || !props.isEnabled
|
|
762
|
+
disabled: (isInputEmpty && !shouldUseTaskPlaceholder) || !props.isEnabled
|
|
644
763
|
}];
|
|
645
764
|
} else if (pending) {
|
|
646
765
|
rightOptions = [{
|
|
@@ -661,21 +780,21 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
661
780
|
}
|
|
662
781
|
},
|
|
663
782
|
className: 'codicon-send',
|
|
664
|
-
disabled: isInputEmpty || !props.isEnabled
|
|
783
|
+
disabled: (isInputEmpty && !shouldUseTaskPlaceholder) || !props.isEnabled
|
|
665
784
|
}];
|
|
666
785
|
}
|
|
667
786
|
|
|
668
787
|
const contextUI = buildContextUI(props.context, props.labelProvider, props.onDeleteContextElement, props.onOpenContextElement);
|
|
669
788
|
|
|
670
789
|
return (
|
|
671
|
-
<div className='theia-ChatInput' onDragOver={props.onDragOver} onDrop={props.onDrop} ref={containerRef}>
|
|
790
|
+
<div className='theia-ChatInput' data-ai-disabled={!props.isEnabled} onDragOver={props.onDragOver} onDrop={props.onDrop} ref={containerRef}>
|
|
672
791
|
{props.showSuggestions !== false && <ChatInputAgentSuggestions suggestions={props.suggestions} opener={props.openerService} />}
|
|
673
792
|
{props.showChangeSet && changeSetUI?.elements &&
|
|
674
793
|
<ChangeSetBox changeSet={changeSetUI} />
|
|
675
794
|
}
|
|
676
795
|
<div className='theia-ChatInput-Editor-Box'>
|
|
677
796
|
<div className='theia-ChatInput-Editor' ref={editorContainerRef} onKeyDown={onKeyDown} onFocus={handleInputFocus} onBlur={handleInputBlur}>
|
|
678
|
-
<div ref={placeholderRef} className='theia-ChatInput-Editor-Placeholder'>{
|
|
797
|
+
<div ref={placeholderRef} className='theia-ChatInput-Editor-Placeholder'>{placeholderText}</div>
|
|
679
798
|
</div>
|
|
680
799
|
{props.context && props.context.length > 0 &&
|
|
681
800
|
<ChatContext context={contextUI.context} />
|
|
@@ -829,7 +948,7 @@ const ChatInputOptions: React.FunctionComponent<ChatInputOptionsProps> = ({ left
|
|
|
829
948
|
{leftOptions.map((option, index) => (
|
|
830
949
|
<span
|
|
831
950
|
key={index}
|
|
832
|
-
className={`option
|
|
951
|
+
className={`option${option.disabled ? ' disabled' : ''}${option.text?.align === 'right' ? ' reverse' : ''}`}
|
|
833
952
|
title={option.title}
|
|
834
953
|
onClick={option.handler}
|
|
835
954
|
>
|
|
@@ -842,7 +961,7 @@ const ChatInputOptions: React.FunctionComponent<ChatInputOptionsProps> = ({ left
|
|
|
842
961
|
{rightOptions.map((option, index) => (
|
|
843
962
|
<span
|
|
844
963
|
key={index}
|
|
845
|
-
className={`option
|
|
964
|
+
className={`option${option.disabled ? ' disabled' : ''}${option.text?.align === 'right' ? ' reverse' : ''}`}
|
|
846
965
|
title={option.title}
|
|
847
966
|
onClick={option.handler}
|
|
848
967
|
>
|
|
@@ -19,11 +19,13 @@ import { inject, injectable } from '@theia/core/shared/inversify';
|
|
|
19
19
|
import { ChatResponseContent, ToolCallChatResponseContent } from '@theia/ai-chat/lib/common';
|
|
20
20
|
import { ReactNode } from '@theia/core/shared/react';
|
|
21
21
|
import { nls } from '@theia/core/lib/common/nls';
|
|
22
|
-
import { codicon } from '@theia/core/lib/browser';
|
|
22
|
+
import { codicon, OpenerService } from '@theia/core/lib/browser';
|
|
23
23
|
import * as React from '@theia/core/shared/react';
|
|
24
24
|
import { ToolConfirmation, ToolConfirmationState } from './tool-confirmation';
|
|
25
25
|
import { ToolConfirmationManager, ToolConfirmationMode } from '@theia/ai-chat/lib/browser/chat-tool-preferences';
|
|
26
26
|
import { ResponseNode } from '../chat-tree-view';
|
|
27
|
+
import { useMarkdownRendering } from './markdown-part-renderer';
|
|
28
|
+
import { ToolCallResult } from '@theia/ai-core';
|
|
27
29
|
|
|
28
30
|
@injectable()
|
|
29
31
|
export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallChatResponseContent> {
|
|
@@ -31,6 +33,9 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
|
|
|
31
33
|
@inject(ToolConfirmationManager)
|
|
32
34
|
protected toolConfirmationManager: ToolConfirmationManager;
|
|
33
35
|
|
|
36
|
+
@inject(OpenerService)
|
|
37
|
+
protected openerService: OpenerService;
|
|
38
|
+
|
|
34
39
|
canHandle(response: ChatResponseContent): number {
|
|
35
40
|
if (ToolCallChatResponseContent.is(response)) {
|
|
36
41
|
return 10;
|
|
@@ -47,7 +52,52 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
|
|
|
47
52
|
toolConfirmationManager={this.toolConfirmationManager}
|
|
48
53
|
chatId={chatId}
|
|
49
54
|
renderCollapsibleArguments={this.renderCollapsibleArguments.bind(this)}
|
|
50
|
-
|
|
55
|
+
responseRenderer={this.renderResult.bind(this)} />;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
protected renderResult(response: ToolCallChatResponseContent): ReactNode {
|
|
59
|
+
const result = this.tryParse(response.result);
|
|
60
|
+
if (!result) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
if (typeof result === 'string') {
|
|
64
|
+
return <pre>{JSON.stringify(result, undefined, 2)}</pre>;
|
|
65
|
+
}
|
|
66
|
+
if ('content' in result) {
|
|
67
|
+
return <div className='theia-toolCall-response-content'>
|
|
68
|
+
{result.content.map((content, idx) => {
|
|
69
|
+
switch (content.type) {
|
|
70
|
+
case 'image': {
|
|
71
|
+
return <div key={`content-${idx}-${content.type}`} className='theia-toolCall-image-result'>
|
|
72
|
+
<img src={`data:${content.mimeType};base64,${content.base64data}`} />
|
|
73
|
+
</div>;
|
|
74
|
+
}
|
|
75
|
+
case 'text': {
|
|
76
|
+
return <div key={`content-${idx}-${content.type}`} className='theia-toolCall-text-result'>
|
|
77
|
+
<MarkdownRender text={content.text} openerService={this.openerService} />
|
|
78
|
+
</div>;
|
|
79
|
+
}
|
|
80
|
+
case 'audio':
|
|
81
|
+
case 'error':
|
|
82
|
+
default: {
|
|
83
|
+
return <div key={`content-${idx}-${content.type}`} className='theia-toolCall-default-result'><pre>{JSON.stringify(response, undefined, 2)}</pre></div>;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
})}
|
|
87
|
+
</div>;
|
|
88
|
+
}
|
|
89
|
+
return <pre>{JSON.stringify(result, undefined, 2)}</pre>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private tryParse(result: ToolCallResult): ToolCallResult {
|
|
93
|
+
if (!result) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
return typeof result === 'string' ? JSON.parse(result) : result;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
51
101
|
}
|
|
52
102
|
|
|
53
103
|
protected getToolConfirmationSettings(responseId: string, chatId: string): ToolConfirmationMode {
|
|
@@ -75,29 +125,6 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
|
|
|
75
125
|
return args;
|
|
76
126
|
}
|
|
77
127
|
}
|
|
78
|
-
|
|
79
|
-
private tryPrettyPrintJson(response: ToolCallChatResponseContent): string | undefined {
|
|
80
|
-
let responseContent = response.result;
|
|
81
|
-
try {
|
|
82
|
-
if (responseContent) {
|
|
83
|
-
if (typeof responseContent === 'string') {
|
|
84
|
-
responseContent = JSON.parse(responseContent);
|
|
85
|
-
}
|
|
86
|
-
responseContent = JSON.stringify(responseContent, undefined, 2);
|
|
87
|
-
}
|
|
88
|
-
} catch (e) {
|
|
89
|
-
if (typeof responseContent !== 'string') {
|
|
90
|
-
responseContent = nls.localize(
|
|
91
|
-
'theia/ai/chat-ui/toolcall-part-renderer/prettyPrintError',
|
|
92
|
-
"The content could not be converted to string: '{0}'. This is the original content: '{1}'.",
|
|
93
|
-
e.message,
|
|
94
|
-
responseContent
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
// fall through
|
|
98
|
-
}
|
|
99
|
-
return responseContent;
|
|
100
|
-
}
|
|
101
128
|
}
|
|
102
129
|
|
|
103
130
|
const Spinner = () => (
|
|
@@ -110,13 +137,13 @@ interface ToolCallContentProps {
|
|
|
110
137
|
toolConfirmationManager: ToolConfirmationManager;
|
|
111
138
|
chatId: string;
|
|
112
139
|
renderCollapsibleArguments: (args: string | undefined) => ReactNode;
|
|
113
|
-
|
|
140
|
+
responseRenderer: (response: ToolCallChatResponseContent) => ReactNode | undefined;
|
|
114
141
|
}
|
|
115
142
|
|
|
116
143
|
/**
|
|
117
144
|
* A function component to handle tool call rendering and confirmation
|
|
118
145
|
*/
|
|
119
|
-
const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmationMode, toolConfirmationManager, chatId,
|
|
146
|
+
const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmationMode, toolConfirmationManager, chatId, responseRenderer, renderCollapsibleArguments }) => {
|
|
120
147
|
const [confirmationState, setConfirmationState] = React.useState<ToolConfirmationState>('waiting');
|
|
121
148
|
|
|
122
149
|
React.useEffect(() => {
|
|
@@ -163,35 +190,43 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmatio
|
|
|
163
190
|
|
|
164
191
|
return (
|
|
165
192
|
<div className='theia-toolCall'>
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
<span className=
|
|
169
|
-
|
|
193
|
+
{confirmationState === 'denied' ? (
|
|
194
|
+
<span className='theia-toolCall-denied'>
|
|
195
|
+
<span className={codicon('error')}></span> {nls.localize('theia/ai/chat-ui/toolcall-part-renderer/denied', 'Execution denied')}: {response.name}
|
|
196
|
+
</span>
|
|
197
|
+
) : response.finished ? (
|
|
198
|
+
<details className='theia-toolCall-finished'>
|
|
199
|
+
<summary>
|
|
200
|
+
{nls.localize('theia/ai/chat-ui/toolcall-part-renderer/finished', 'Ran')} {response.name}
|
|
201
|
+
({renderCollapsibleArguments(response.arguments)})
|
|
202
|
+
</summary>
|
|
203
|
+
<div className='theia-toolCall-response-result'>
|
|
204
|
+
{responseRenderer(response)}
|
|
205
|
+
</div>
|
|
206
|
+
</details>
|
|
207
|
+
) : (
|
|
208
|
+
confirmationState === 'allowed' && (
|
|
209
|
+
<span className='theia-toolCall-allowed'>
|
|
210
|
+
<Spinner /> {nls.localizeByDefault('Running')} {response.name}
|
|
170
211
|
</span>
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
<summary>{nls.localize('theia/ai/chat-ui/toolcall-part-renderer/finished', 'Ran')} {response.name}
|
|
174
|
-
({renderCollapsibleArguments(response.arguments)})
|
|
175
|
-
</summary>
|
|
176
|
-
<pre>{tryPrettyPrintJson(response)}</pre>
|
|
177
|
-
</details>
|
|
178
|
-
) : (
|
|
179
|
-
confirmationState === 'allowed' && (
|
|
180
|
-
<span>
|
|
181
|
-
<Spinner /> {nls.localizeByDefault('Running')} {response.name}
|
|
182
|
-
</span>
|
|
183
|
-
)
|
|
184
|
-
)}
|
|
185
|
-
</h4>
|
|
212
|
+
)
|
|
213
|
+
)}
|
|
186
214
|
|
|
187
215
|
{/* Show confirmation UI when waiting for allow */}
|
|
188
216
|
{confirmationState === 'waiting' && (
|
|
189
|
-
<
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
217
|
+
<span className='theia-toolCall-waiting'>
|
|
218
|
+
<ToolConfirmation
|
|
219
|
+
response={response}
|
|
220
|
+
onAllow={handleAllow}
|
|
221
|
+
onDeny={handleDeny}
|
|
222
|
+
/>
|
|
223
|
+
</span>
|
|
194
224
|
)}
|
|
195
225
|
</div>
|
|
196
226
|
);
|
|
197
227
|
};
|
|
228
|
+
|
|
229
|
+
const MarkdownRender = ({ text, openerService }: { text: string; openerService: OpenerService }) => {
|
|
230
|
+
const ref = useMarkdownRendering(text, openerService);
|
|
231
|
+
return <div ref={ref}></div>;
|
|
232
|
+
};
|