@theia/ai-chat-ui 1.63.0-next.24 → 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 +31 -1
- package/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -1
- package/lib/browser/ai-chat-ui-contribution.js +192 -8
- 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 +24 -16
- package/lib/browser/chat-input-widget.d.ts.map +1 -1
- package/lib/browser/chat-input-widget.js +199 -37
- 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 +1 -0
- package/lib/browser/chat-response-renderer/index.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/index.js +1 -0
- package/lib/browser/chat-response-renderer/index.js.map +1 -1
- package/lib/browser/chat-response-renderer/tool-confirmation.d.ts +2 -2
- package/lib/browser/chat-response-renderer/tool-confirmation.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/tool-confirmation.js +23 -23
- package/lib/browser/chat-response-renderer/tool-confirmation.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 +65 -32
- 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 +3 -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 +50 -8
- 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.d.ts +2 -0
- package/lib/browser/chat-view-contribution.d.ts.map +1 -1
- package/lib/browser/chat-view-contribution.js +31 -18
- 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/lib/browser/chat-view-widget.d.ts +1 -1
- package/lib/browser/chat-view-widget.d.ts.map +1 -1
- package/lib/browser/chat-view-widget.js +1 -4
- package/lib/browser/chat-view-widget.js.map +1 -1
- package/package.json +11 -11
- package/src/browser/ai-chat-ui-contribution.ts +191 -12
- package/src/browser/ai-chat-ui-frontend-module.ts +11 -0
- package/src/browser/chat-input-widget.tsx +253 -58
- 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 +1 -0
- package/src/browser/chat-response-renderer/tool-confirmation.tsx +30 -30
- package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +95 -60
- package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +58 -8
- 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 +29 -18
- package/src/browser/chat-view-widget-toolbar-contribution.tsx +12 -5
- package/src/browser/chat-view-widget.tsx +2 -5
- package/src/browser/style/index.css +209 -5
|
@@ -14,24 +14,27 @@
|
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
import {
|
|
17
|
-
ChangeSet, ChangeSetElement, ChatAgent, ChatChangeEvent,
|
|
18
|
-
ChatService, ChatSuggestion, EditableChatRequestModel
|
|
17
|
+
ChangeSet, ChangeSetElement, ChatAgent, ChatChangeEvent, ChatHierarchyBranch,
|
|
18
|
+
ChatModel, ChatRequestModel, ChatService, ChatSuggestion, EditableChatRequestModel
|
|
19
19
|
} from '@theia/ai-chat';
|
|
20
|
-
import {
|
|
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 { AgentCompletionNotificationService, FrontendVariableService, AIActivationService } from '@theia/ai-core/lib/browser';
|
|
24
|
+
import { DisposableCollection, Emitter, InMemoryResources, URI, nls } from '@theia/core';
|
|
21
25
|
import { ContextMenuRenderer, LabelProvider, Message, OpenerService, ReactWidget } from '@theia/core/lib/browser';
|
|
22
26
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
23
27
|
import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify';
|
|
24
28
|
import * as React from '@theia/core/shared/react';
|
|
25
|
-
import { IMouseEvent } from '@theia/monaco-editor-core';
|
|
26
|
-
import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
|
|
29
|
+
import { IMouseEvent, Range } from '@theia/monaco-editor-core';
|
|
27
30
|
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
|
|
28
|
-
import {
|
|
29
|
-
import { AIVariableResolutionRequest } from '@theia/ai-core';
|
|
30
|
-
import { FrontendVariableService } from '@theia/ai-core/lib/browser';
|
|
31
|
-
import { ContextVariablePicker } from './context-variable-picker';
|
|
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
|
-
import { ChangeSetDecoratorService } from '@theia/ai-chat/lib/browser/change-set-decorator-service';
|
|
34
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';
|
|
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';
|
|
35
38
|
|
|
36
39
|
type Query = (query: string) => Promise<void>;
|
|
37
40
|
type Unpin = () => void;
|
|
@@ -77,6 +80,9 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
77
80
|
@inject(ChangeSetActionService)
|
|
78
81
|
protected readonly changeSetActionService: ChangeSetActionService;
|
|
79
82
|
|
|
83
|
+
@inject(AgentCompletionNotificationService)
|
|
84
|
+
protected readonly agentNotificationService: AgentCompletionNotificationService;
|
|
85
|
+
|
|
80
86
|
@inject(ChangeSetDecoratorService)
|
|
81
87
|
protected readonly changeSetDecoratorService: ChangeSetDecoratorService;
|
|
82
88
|
|
|
@@ -86,12 +92,16 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
86
92
|
@inject(ChatService)
|
|
87
93
|
protected readonly chatService: ChatService;
|
|
88
94
|
|
|
95
|
+
@inject(AIActivationService)
|
|
96
|
+
protected readonly aiActivationService: AIActivationService;
|
|
97
|
+
|
|
89
98
|
protected editorRef: SimpleMonacoEditor | undefined = undefined;
|
|
90
99
|
protected readonly editorReady = new Deferred<void>();
|
|
91
100
|
|
|
92
101
|
protected isEnabled = false;
|
|
102
|
+
protected heightInLines = 12;
|
|
93
103
|
|
|
94
|
-
|
|
104
|
+
protected _branch?: ChatHierarchyBranch;
|
|
95
105
|
set branch(branch: ChatHierarchyBranch | undefined) {
|
|
96
106
|
if (this._branch !== branch) {
|
|
97
107
|
this._branch = branch;
|
|
@@ -99,34 +109,34 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
99
109
|
}
|
|
100
110
|
}
|
|
101
111
|
|
|
102
|
-
|
|
112
|
+
protected _onQuery: Query;
|
|
103
113
|
set onQuery(query: Query) {
|
|
104
114
|
this._onQuery = query;
|
|
105
115
|
}
|
|
106
|
-
|
|
116
|
+
protected _onUnpin: Unpin;
|
|
107
117
|
set onUnpin(unpin: Unpin) {
|
|
108
118
|
this._onUnpin = unpin;
|
|
109
119
|
}
|
|
110
|
-
|
|
120
|
+
protected _onCancel: Cancel;
|
|
111
121
|
set onCancel(cancel: Cancel) {
|
|
112
122
|
this._onCancel = cancel;
|
|
113
123
|
}
|
|
114
|
-
|
|
124
|
+
protected _onDeleteChangeSet: DeleteChangeSet;
|
|
115
125
|
set onDeleteChangeSet(deleteChangeSet: DeleteChangeSet) {
|
|
116
126
|
this._onDeleteChangeSet = deleteChangeSet;
|
|
117
127
|
}
|
|
118
|
-
|
|
128
|
+
protected _onDeleteChangeSetElement: DeleteChangeSetElement;
|
|
119
129
|
set onDeleteChangeSetElement(deleteChangeSetElement: DeleteChangeSetElement) {
|
|
120
130
|
this._onDeleteChangeSetElement = deleteChangeSetElement;
|
|
121
131
|
}
|
|
122
132
|
|
|
123
|
-
|
|
133
|
+
protected _initialValue?: string;
|
|
124
134
|
set initialValue(value: string | undefined) {
|
|
125
135
|
this._initialValue = value;
|
|
126
136
|
}
|
|
127
137
|
|
|
128
138
|
protected onDisposeForChatModel = new DisposableCollection();
|
|
129
|
-
|
|
139
|
+
protected _chatModel: ChatModel;
|
|
130
140
|
set chatModel(chatModel: ChatModel) {
|
|
131
141
|
this.onDisposeForChatModel.dispose();
|
|
132
142
|
this.onDisposeForChatModel = new DisposableCollection();
|
|
@@ -138,17 +148,25 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
138
148
|
this._chatModel = chatModel;
|
|
139
149
|
this.update();
|
|
140
150
|
}
|
|
141
|
-
|
|
151
|
+
protected _pinnedAgent: ChatAgent | undefined;
|
|
142
152
|
set pinnedAgent(pinnedAgent: ChatAgent | undefined) {
|
|
143
153
|
this._pinnedAgent = pinnedAgent;
|
|
144
154
|
this.update();
|
|
145
155
|
}
|
|
146
156
|
|
|
157
|
+
protected onDidResizeEmitter = new Emitter<void>();
|
|
158
|
+
readonly onDidResize = this.onDidResizeEmitter.event;
|
|
159
|
+
|
|
147
160
|
@postConstruct()
|
|
148
161
|
protected init(): void {
|
|
149
162
|
this.id = AIChatInputWidget.ID;
|
|
150
163
|
this.title.closable = false;
|
|
151
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);
|
|
152
170
|
this.update();
|
|
153
171
|
}
|
|
154
172
|
|
|
@@ -161,6 +179,18 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
161
179
|
});
|
|
162
180
|
}
|
|
163
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
|
+
|
|
164
194
|
protected getResourceUri(): URI {
|
|
165
195
|
return new URI(`ai-chat:/input.${CHAT_VIEW_LANGUAGE_EXTENSION}`);
|
|
166
196
|
}
|
|
@@ -185,12 +215,15 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
185
215
|
onCancel={this._onCancel.bind(this)}
|
|
186
216
|
onDragOver={this.onDragOver.bind(this)}
|
|
187
217
|
onDrop={this.onDrop.bind(this)}
|
|
218
|
+
onPaste={this.onPaste.bind(this)}
|
|
219
|
+
onEscape={this.onEscape.bind(this)}
|
|
188
220
|
onDeleteChangeSet={this._onDeleteChangeSet.bind(this)}
|
|
189
221
|
onDeleteChangeSetElement={this._onDeleteChangeSetElement.bind(this)}
|
|
190
222
|
onAddContextElement={this.addContextElement.bind(this)}
|
|
191
223
|
onDeleteContextElement={this.deleteContextElement.bind(this)}
|
|
192
224
|
onOpenContextElement={this.openContextElement.bind(this)}
|
|
193
225
|
context={this.getContext()}
|
|
226
|
+
onAgentCompletion={this.handleAgentCompletion.bind(this)}
|
|
194
227
|
chatModel={this._chatModel}
|
|
195
228
|
pinnedAgent={this._pinnedAgent}
|
|
196
229
|
editorProvider={this.editorProvider}
|
|
@@ -214,11 +247,13 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
214
247
|
currentRequest={currentRequest}
|
|
215
248
|
isEditing={isEditing}
|
|
216
249
|
pending={pending}
|
|
250
|
+
heightInLines={this.heightInLines}
|
|
217
251
|
onResponseChanged={() => {
|
|
218
252
|
if (isPending() !== pending) {
|
|
219
253
|
this.update();
|
|
220
254
|
}
|
|
221
255
|
}}
|
|
256
|
+
onResize={() => this.onDidResizeEmitter.fire()}
|
|
222
257
|
/>
|
|
223
258
|
);
|
|
224
259
|
}
|
|
@@ -257,6 +292,30 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
257
292
|
});
|
|
258
293
|
}
|
|
259
294
|
|
|
295
|
+
protected onPaste(event: ClipboardEvent): void {
|
|
296
|
+
this.variableService.getPasteResult(event, { type: 'ai-chat-input-widget' }).then(result => {
|
|
297
|
+
result.variables.forEach(variable => this.addContext(variable));
|
|
298
|
+
if (result.text) {
|
|
299
|
+
const position = this.editorRef?.getControl().getPosition();
|
|
300
|
+
if (position && result.text) {
|
|
301
|
+
this.editorRef?.getControl().executeEdits('paste', [{
|
|
302
|
+
range: {
|
|
303
|
+
startLineNumber: position.lineNumber,
|
|
304
|
+
startColumn: position.column,
|
|
305
|
+
endLineNumber: position.lineNumber,
|
|
306
|
+
endColumn: position.column
|
|
307
|
+
},
|
|
308
|
+
text: result.text
|
|
309
|
+
}]);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
protected onEscape(): void {
|
|
316
|
+
// No op
|
|
317
|
+
}
|
|
318
|
+
|
|
260
319
|
protected async openContextElement(request: AIVariableResolutionRequest): Promise<void> {
|
|
261
320
|
const session = this.chatService.getSessions().find(candidate => candidate.model.id === this._chatModel.id);
|
|
262
321
|
const context = { session };
|
|
@@ -306,11 +365,14 @@ interface ChatInputProperties {
|
|
|
306
365
|
onUnpin: () => void;
|
|
307
366
|
onDragOver: (event: React.DragEvent) => void;
|
|
308
367
|
onDrop: (event: React.DragEvent) => void;
|
|
368
|
+
onPaste: (event: ClipboardEvent) => void;
|
|
309
369
|
onDeleteChangeSet: (sessionId: string) => void;
|
|
310
370
|
onDeleteChangeSetElement: (sessionId: string, uri: URI) => void;
|
|
311
371
|
onAddContextElement: () => void;
|
|
312
372
|
onDeleteContextElement: (index: number) => void;
|
|
373
|
+
onEscape: () => void;
|
|
313
374
|
onOpenContextElement: OpenContextElement;
|
|
375
|
+
onAgentCompletion: (request: ChatRequestModel) => void;
|
|
314
376
|
context?: readonly AIVariableResolutionRequest[];
|
|
315
377
|
isEnabled?: boolean;
|
|
316
378
|
chatModel: ChatModel;
|
|
@@ -332,9 +394,16 @@ interface ChatInputProperties {
|
|
|
332
394
|
currentRequest?: ChatRequestModel;
|
|
333
395
|
isEditing: boolean;
|
|
334
396
|
pending: boolean;
|
|
397
|
+
heightInLines?: number;
|
|
335
398
|
onResponseChanged: () => void;
|
|
399
|
+
onResize: () => void;
|
|
336
400
|
}
|
|
337
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
|
+
|
|
338
407
|
const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInputProperties) => {
|
|
339
408
|
const onDeleteChangeSet = () => props.onDeleteChangeSet(props.chatModel.id);
|
|
340
409
|
const onDeleteChangeSetElement = (uri: URI) => props.onDeleteChangeSetElement(props.chatModel.id, uri);
|
|
@@ -355,13 +424,44 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
355
424
|
// eslint-disable-next-line no-null/no-null
|
|
356
425
|
const placeholderRef = React.useRef<HTMLDivElement | null>(null);
|
|
357
426
|
const editorRef = React.useRef<SimpleMonacoEditor | undefined>(undefined);
|
|
427
|
+
// eslint-disable-next-line no-null/no-null
|
|
428
|
+
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
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
|
+
|
|
441
|
+
// Handle paste events on the container
|
|
442
|
+
const handlePaste = React.useCallback((event: ClipboardEvent) => {
|
|
443
|
+
props.onPaste(event);
|
|
444
|
+
}, [props.onPaste]);
|
|
445
|
+
|
|
446
|
+
// Set up paste handler on the container div
|
|
447
|
+
React.useEffect(() => {
|
|
448
|
+
const container = containerRef.current;
|
|
449
|
+
if (container) {
|
|
450
|
+
container.addEventListener('paste', handlePaste, true);
|
|
451
|
+
return () => {
|
|
452
|
+
container.removeEventListener('paste', handlePaste, true);
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
return undefined;
|
|
456
|
+
}, [handlePaste]);
|
|
358
457
|
|
|
359
458
|
React.useEffect(() => {
|
|
360
459
|
const uri = props.uri;
|
|
361
460
|
const createInputElement = async () => {
|
|
362
461
|
const paddingTop = 6;
|
|
363
462
|
const lineHeight = 20;
|
|
364
|
-
const
|
|
463
|
+
const maxHeightPx = (props.heightInLines ?? 12) * lineHeight;
|
|
464
|
+
|
|
365
465
|
const editor = await props.editorProvider.createSimpleInline(uri, editorContainerRef.current!, {
|
|
366
466
|
language: CHAT_VIEW_LANGUAGE_EXTENSION,
|
|
367
467
|
// Disable code lens, inlay hints and hover support to avoid console errors from other contributions
|
|
@@ -396,12 +496,17 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
396
496
|
if (editorContainerRef.current) {
|
|
397
497
|
editorContainerRef.current.style.overflowY = 'auto'; // ensure vertical scrollbar
|
|
398
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 });
|
|
399
504
|
}
|
|
400
505
|
|
|
401
506
|
const updateEditorHeight = () => {
|
|
402
507
|
if (editorContainerRef.current) {
|
|
403
508
|
const contentHeight = editor.getControl().getContentHeight() + paddingTop;
|
|
404
|
-
editorContainerRef.current.style.height = `${Math.min(contentHeight,
|
|
509
|
+
editorContainerRef.current.style.height = `${Math.min(contentHeight, maxHeightPx)}px`;
|
|
405
510
|
}
|
|
406
511
|
};
|
|
407
512
|
|
|
@@ -411,7 +516,10 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
411
516
|
updateEditorHeight();
|
|
412
517
|
handleOnChange();
|
|
413
518
|
});
|
|
414
|
-
const resizeObserver = new ResizeObserver(
|
|
519
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
520
|
+
updateEditorHeight();
|
|
521
|
+
props.onResize();
|
|
522
|
+
});
|
|
415
523
|
if (editorContainerRef.current) {
|
|
416
524
|
resizeObserver.observe(editorContainerRef.current);
|
|
417
525
|
}
|
|
@@ -423,12 +531,46 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
423
531
|
props.contextMenuCallback(e.event)
|
|
424
532
|
);
|
|
425
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
|
+
|
|
426
566
|
editorRef.current = editor;
|
|
427
567
|
props.setEditorRef(editor);
|
|
428
568
|
|
|
429
569
|
if (props.initialValue) {
|
|
430
570
|
setValue(props.initialValue);
|
|
431
571
|
}
|
|
572
|
+
|
|
573
|
+
updateLineCounts();
|
|
432
574
|
};
|
|
433
575
|
createInputElement();
|
|
434
576
|
|
|
@@ -460,6 +602,15 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
460
602
|
onDeleteChangeSetElement
|
|
461
603
|
));
|
|
462
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
|
+
}
|
|
463
614
|
});
|
|
464
615
|
return () => {
|
|
465
616
|
listener.dispose();
|
|
@@ -472,7 +623,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
472
623
|
setChangeSetUI(current => !current ? current : { ...current, actions: newActions });
|
|
473
624
|
});
|
|
474
625
|
return () => disposable.dispose();
|
|
475
|
-
});
|
|
626
|
+
}, [props.actionService, props.chatModel.changeSet]);
|
|
476
627
|
|
|
477
628
|
React.useEffect(() => {
|
|
478
629
|
const disposable = props.decoratorService.onDidChangeDecorations(() => {
|
|
@@ -494,13 +645,21 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
494
645
|
}
|
|
495
646
|
}, [editorRef]);
|
|
496
647
|
|
|
648
|
+
// Without user input, if we can default to "Perform this task.", do so
|
|
497
649
|
const submit = React.useCallback(function submit(value: string): void {
|
|
498
|
-
|
|
650
|
+
let effectiveValue = value;
|
|
651
|
+
if ((!value || value.trim().length === 0) && shouldUseTaskPlaceholder) {
|
|
652
|
+
effectiveValue = taskPlaceholder;
|
|
653
|
+
}
|
|
654
|
+
if (!effectiveValue || effectiveValue.trim().length === 0) {
|
|
499
655
|
return;
|
|
500
656
|
}
|
|
501
|
-
props.onQuery(
|
|
657
|
+
props.onQuery(effectiveValue);
|
|
502
658
|
setValue('');
|
|
503
|
-
|
|
659
|
+
if (editorRef.current && !editorRef.current.document.textEditorModel.isDisposed()) {
|
|
660
|
+
editorRef.current.document.textEditorModel.setValue('');
|
|
661
|
+
}
|
|
662
|
+
}, [props.context, props.onQuery, setValue, shouldUseTaskPlaceholder, taskPlaceholder]);
|
|
504
663
|
|
|
505
664
|
const onKeyDown = React.useCallback((event: React.KeyboardEvent) => {
|
|
506
665
|
if (!props.isEnabled) {
|
|
@@ -508,7 +667,12 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
508
667
|
}
|
|
509
668
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
510
669
|
event.preventDefault();
|
|
511
|
-
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();
|
|
512
676
|
}
|
|
513
677
|
}, [props.isEnabled, submit]);
|
|
514
678
|
|
|
@@ -559,7 +723,8 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
559
723
|
? [{
|
|
560
724
|
title: nls.localize('theia/ai/chat-ui/attachToContext', 'Attach elements to context'),
|
|
561
725
|
handler: () => props.onAddContextElement(),
|
|
562
|
-
className: 'codicon-add'
|
|
726
|
+
className: 'codicon-add',
|
|
727
|
+
disabled: !props.isEnabled
|
|
563
728
|
}]
|
|
564
729
|
: []),
|
|
565
730
|
...(props.showPinnedAgent
|
|
@@ -567,6 +732,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
567
732
|
title: props.pinnedAgent ? nls.localize('theia/ai/chat-ui/unpinAgent', 'Unpin Agent') : nls.localize('theia/ai/chat-ui/pinAgent', 'Pin Agent'),
|
|
568
733
|
handler: props.pinnedAgent ? props.onUnpin : handlePin,
|
|
569
734
|
className: 'at-icon',
|
|
735
|
+
disabled: !props.isEnabled,
|
|
570
736
|
text: {
|
|
571
737
|
align: 'right',
|
|
572
738
|
content: props.pinnedAgent && props.pinnedAgent.name
|
|
@@ -593,7 +759,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
593
759
|
}
|
|
594
760
|
},
|
|
595
761
|
className: 'codicon-send',
|
|
596
|
-
disabled: isInputEmpty || !props.isEnabled
|
|
762
|
+
disabled: (isInputEmpty && !shouldUseTaskPlaceholder) || !props.isEnabled
|
|
597
763
|
}];
|
|
598
764
|
} else if (pending) {
|
|
599
765
|
rightOptions = [{
|
|
@@ -614,27 +780,29 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
614
780
|
}
|
|
615
781
|
},
|
|
616
782
|
className: 'codicon-send',
|
|
617
|
-
disabled: isInputEmpty || !props.isEnabled
|
|
783
|
+
disabled: (isInputEmpty && !shouldUseTaskPlaceholder) || !props.isEnabled
|
|
618
784
|
}];
|
|
619
785
|
}
|
|
620
786
|
|
|
621
787
|
const contextUI = buildContextUI(props.context, props.labelProvider, props.onDeleteContextElement, props.onOpenContextElement);
|
|
622
788
|
|
|
623
|
-
return
|
|
624
|
-
{props.
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
<div className='theia-ChatInput-Editor-Box'>
|
|
629
|
-
<div className='theia-ChatInput-Editor' ref={editorContainerRef} onKeyDown={onKeyDown} onFocus={handleInputFocus} onBlur={handleInputBlur}>
|
|
630
|
-
<div ref={placeholderRef} className='theia-ChatInput-Editor-Placeholder'>{nls.localizeByDefault('Ask a question')}</div>
|
|
631
|
-
</div>
|
|
632
|
-
{props.context && props.context.length > 0 &&
|
|
633
|
-
<ChatContext context={contextUI.context} />
|
|
789
|
+
return (
|
|
790
|
+
<div className='theia-ChatInput' data-ai-disabled={!props.isEnabled} onDragOver={props.onDragOver} onDrop={props.onDrop} ref={containerRef}>
|
|
791
|
+
{props.showSuggestions !== false && <ChatInputAgentSuggestions suggestions={props.suggestions} opener={props.openerService} />}
|
|
792
|
+
{props.showChangeSet && changeSetUI?.elements &&
|
|
793
|
+
<ChangeSetBox changeSet={changeSetUI} />
|
|
634
794
|
}
|
|
635
|
-
<
|
|
795
|
+
<div className='theia-ChatInput-Editor-Box'>
|
|
796
|
+
<div className='theia-ChatInput-Editor' ref={editorContainerRef} onKeyDown={onKeyDown} onFocus={handleInputFocus} onBlur={handleInputBlur}>
|
|
797
|
+
<div ref={placeholderRef} className='theia-ChatInput-Editor-Placeholder'>{placeholderText}</div>
|
|
798
|
+
</div>
|
|
799
|
+
{props.context && props.context.length > 0 &&
|
|
800
|
+
<ChatContext context={contextUI.context} />
|
|
801
|
+
}
|
|
802
|
+
<ChatInputOptions leftOptions={leftOptions} rightOptions={rightOptions} />
|
|
803
|
+
</div>
|
|
636
804
|
</div>
|
|
637
|
-
|
|
805
|
+
);
|
|
638
806
|
};
|
|
639
807
|
|
|
640
808
|
const noPropagation = (handler: () => void) => (e: React.MouseEvent) => {
|
|
@@ -780,7 +948,7 @@ const ChatInputOptions: React.FunctionComponent<ChatInputOptionsProps> = ({ left
|
|
|
780
948
|
{leftOptions.map((option, index) => (
|
|
781
949
|
<span
|
|
782
950
|
key={index}
|
|
783
|
-
className={`option
|
|
951
|
+
className={`option${option.disabled ? ' disabled' : ''}${option.text?.align === 'right' ? ' reverse' : ''}`}
|
|
784
952
|
title={option.title}
|
|
785
953
|
onClick={option.handler}
|
|
786
954
|
>
|
|
@@ -793,7 +961,7 @@ const ChatInputOptions: React.FunctionComponent<ChatInputOptionsProps> = ({ left
|
|
|
793
961
|
{rightOptions.map((option, index) => (
|
|
794
962
|
<span
|
|
795
963
|
key={index}
|
|
796
|
-
className={`option
|
|
964
|
+
className={`option${option.disabled ? ' disabled' : ''}${option.text?.align === 'right' ? ' reverse' : ''}`}
|
|
797
965
|
title={option.title}
|
|
798
966
|
onClick={option.handler}
|
|
799
967
|
>
|
|
@@ -816,6 +984,7 @@ function buildContextUI(
|
|
|
816
984
|
}
|
|
817
985
|
return {
|
|
818
986
|
context: context.map((element, index) => ({
|
|
987
|
+
variable: element,
|
|
819
988
|
name: labelProvider.getName(element),
|
|
820
989
|
iconClass: labelProvider.getIcon(element),
|
|
821
990
|
nameClass: element.variable.name,
|
|
@@ -829,6 +998,7 @@ function buildContextUI(
|
|
|
829
998
|
|
|
830
999
|
interface ChatContextUI {
|
|
831
1000
|
context: {
|
|
1001
|
+
variable: AIVariableResolutionRequest,
|
|
832
1002
|
name: string;
|
|
833
1003
|
iconClass: string;
|
|
834
1004
|
nameClass: string;
|
|
@@ -842,20 +1012,45 @@ interface ChatContextUI {
|
|
|
842
1012
|
const ChatContext: React.FunctionComponent<ChatContextUI> = ({ context }) => (
|
|
843
1013
|
<div className="theia-ChatInput-ChatContext">
|
|
844
1014
|
<ul>
|
|
845
|
-
{context.map((element, index) =>
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
<
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1015
|
+
{context.map((element, index) => {
|
|
1016
|
+
if (ImageContextVariable.isImageContextRequest(element.variable)) {
|
|
1017
|
+
const variable = ImageContextVariable.parseRequest(element.variable)!;
|
|
1018
|
+
return <li key={index} className="theia-ChatInput-ChatContext-Element theia-ChatInput-ImageContext-Element"
|
|
1019
|
+
title={variable.name ?? variable.wsRelativePath} onClick={() => element.open?.()}>
|
|
1020
|
+
<div className="theia-ChatInput-ChatContext-Row">
|
|
1021
|
+
<div className={`theia-ChatInput-ChatContext-Icon ${element.iconClass}`} />
|
|
1022
|
+
<div className="theia-ChatInput-ChatContext-labelParts">
|
|
1023
|
+
<span className={`theia-ChatInput-ChatContext-title ${element.nameClass}`}>
|
|
1024
|
+
{variable.name ?? variable.wsRelativePath?.split('/').pop()}
|
|
1025
|
+
</span>
|
|
1026
|
+
<span className='theia-ChatInput-ChatContext-additionalInfo'>
|
|
1027
|
+
{element.additionalInfo}
|
|
1028
|
+
</span>
|
|
1029
|
+
</div>
|
|
1030
|
+
<span className="codicon codicon-close action" title={nls.localizeByDefault('Delete')} onClick={e => { e.stopPropagation(); element.delete(); }} />
|
|
1031
|
+
</div>
|
|
1032
|
+
<div className="theia-ChatInput-ChatContext-ImageRow">
|
|
1033
|
+
<div className='theia-ChatInput-ImagePreview-Item'>
|
|
1034
|
+
<img src={`data:${variable.mimeType};base64,${variable.data}`} alt={variable.name} />
|
|
1035
|
+
</div>
|
|
1036
|
+
</div>
|
|
1037
|
+
</li>;
|
|
1038
|
+
}
|
|
1039
|
+
return <li key={index} className="theia-ChatInput-ChatContext-Element" title={element.details} onClick={() => element.open?.()}>
|
|
1040
|
+
<div className="theia-ChatInput-ChatContext-Row">
|
|
1041
|
+
<div className={`theia-ChatInput-ChatContext-Icon ${element.iconClass}`} />
|
|
1042
|
+
<div className="theia-ChatInput-ChatContext-labelParts">
|
|
1043
|
+
<span className={`theia-ChatInput-ChatContext-title ${element.nameClass}`}>
|
|
1044
|
+
{element.name}
|
|
1045
|
+
</span>
|
|
1046
|
+
<span className='theia-ChatInput-ChatContext-additionalInfo'>
|
|
1047
|
+
{element.additionalInfo}
|
|
1048
|
+
</span>
|
|
1049
|
+
</div>
|
|
1050
|
+
<span className="codicon codicon-close action" title={nls.localizeByDefault('Delete')} onClick={e => { e.stopPropagation(); element.delete(); }} />
|
|
855
1051
|
</div>
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
))}
|
|
1052
|
+
</li>;
|
|
1053
|
+
})}
|
|
859
1054
|
</ul>
|
|
860
1055
|
</div>
|
|
861
1056
|
);
|
|
@@ -78,6 +78,11 @@ export namespace ChatNodeToolbarCommands {
|
|
|
78
78
|
id: 'chat:node:toolbar:cancel-request',
|
|
79
79
|
category: CHAT_NODE_TOOLBAR_CATEGORY,
|
|
80
80
|
}, '', CHAT_NODE_TOOLBAR_CATEGORY_KEY);
|
|
81
|
+
|
|
82
|
+
export const RETRY = Command.toLocalizedCommand({
|
|
83
|
+
id: 'chat:node:toolbar:retry-message',
|
|
84
|
+
category: CHAT_NODE_TOOLBAR_CATEGORY,
|
|
85
|
+
}, 'Retry', CHAT_NODE_TOOLBAR_CATEGORY_KEY);
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
export class DefaultChatNodeToolbarActionContribution implements ChatNodeToolbarActionContribution {
|
|
@@ -96,6 +101,15 @@ export class DefaultChatNodeToolbarActionContribution implements ChatNodeToolbar
|
|
|
96
101
|
tooltip: nls.localize('theia/ai/chat-ui/node/toolbar/edit', 'Edit'),
|
|
97
102
|
}];
|
|
98
103
|
} else {
|
|
104
|
+
const shouldShowRetry = node.response.isError || node.response.isCanceled;
|
|
105
|
+
if (shouldShowRetry) {
|
|
106
|
+
return [{
|
|
107
|
+
commandId: ChatNodeToolbarCommands.RETRY.id,
|
|
108
|
+
icon: codicon('refresh'),
|
|
109
|
+
tooltip: nls.localize('theia/ai/chat-ui/node/toolbar/retry', 'Retry'),
|
|
110
|
+
priority: -1 // Higher priority to show it first
|
|
111
|
+
}];
|
|
112
|
+
}
|
|
99
113
|
return [];
|
|
100
114
|
}
|
|
101
115
|
}
|