@theia/ai-chat-ui 1.62.1 → 1.63.0-next.24
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.js +2 -2
- package/lib/browser/ai-chat-ui-contribution.js.map +1 -1
- package/lib/browser/chat-input-widget.d.ts +3 -1
- package/lib/browser/chat-input-widget.d.ts.map +1 -1
- package/lib/browser/chat-input-widget.js +101 -90
- package/lib/browser/chat-input-widget.js.map +1 -1
- 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 +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 +6 -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 +30 -3
- package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
- package/lib/browser/chat-view-widget.d.ts +5 -2
- package/lib/browser/chat-view-widget.d.ts.map +1 -1
- package/lib/browser/chat-view-widget.js +19 -6
- package/lib/browser/chat-view-widget.js.map +1 -1
- package/package.json +11 -11
- package/src/browser/ai-chat-ui-contribution.ts +2 -2
- package/src/browser/chat-input-widget.tsx +171 -137
- package/src/browser/chat-response-renderer/index.ts +1 -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 +39 -5
- package/src/browser/chat-view-widget.tsx +23 -7
- package/src/browser/style/index.css +173 -0
|
@@ -13,8 +13,11 @@
|
|
|
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, ChatModel, ChatRequestModel,
|
|
18
|
+
ChatService, ChatSuggestion, EditableChatRequestModel, ChatHierarchyBranch
|
|
19
|
+
} from '@theia/ai-chat';
|
|
20
|
+
import { DisposableCollection, InMemoryResources, URI, nls } from '@theia/core';
|
|
18
21
|
import { ContextMenuRenderer, LabelProvider, Message, OpenerService, ReactWidget } from '@theia/core/lib/browser';
|
|
19
22
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
20
23
|
import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify';
|
|
@@ -88,6 +91,14 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
88
91
|
|
|
89
92
|
protected isEnabled = false;
|
|
90
93
|
|
|
94
|
+
private _branch?: ChatHierarchyBranch;
|
|
95
|
+
set branch(branch: ChatHierarchyBranch | undefined) {
|
|
96
|
+
if (this._branch !== branch) {
|
|
97
|
+
this._branch = branch;
|
|
98
|
+
this.update();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
91
102
|
private _onQuery: Query;
|
|
92
103
|
set onQuery(query: Query) {
|
|
93
104
|
this._onQuery = query;
|
|
@@ -120,7 +131,7 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
120
131
|
this.onDisposeForChatModel.dispose();
|
|
121
132
|
this.onDisposeForChatModel = new DisposableCollection();
|
|
122
133
|
this.onDisposeForChatModel.push(chatModel.onDidChange(event => {
|
|
123
|
-
if (event.kind === 'addVariable' || event.kind === 'removeVariable') {
|
|
134
|
+
if (event.kind === 'addVariable' || event.kind === 'removeVariable' || event.kind === 'addRequest' || event.kind === 'changeHierarchyBranch') {
|
|
124
135
|
this.update();
|
|
125
136
|
}
|
|
126
137
|
}));
|
|
@@ -137,6 +148,7 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
137
148
|
protected init(): void {
|
|
138
149
|
this.id = AIChatInputWidget.ID;
|
|
139
150
|
this.title.closable = false;
|
|
151
|
+
this.toDispose.push(this.resources.add(this.getResourceUri(), ''));
|
|
140
152
|
this.update();
|
|
141
153
|
}
|
|
142
154
|
|
|
@@ -154,8 +166,20 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
154
166
|
}
|
|
155
167
|
|
|
156
168
|
protected render(): React.ReactNode {
|
|
169
|
+
const branch = this._branch;
|
|
170
|
+
const chatModel = this._chatModel;
|
|
171
|
+
|
|
172
|
+
// State of the input widget's action buttons depends on the state of the currently active or last processed
|
|
173
|
+
// request, if there is one. If the chat model has branched, then the current request is the last on the
|
|
174
|
+
// branch. Otherwise, it's the last request in the chat model.
|
|
175
|
+
const currentRequest: ChatRequestModel | undefined = branch?.items?.at(-1)?.element ?? chatModel.getRequests().at(-1);
|
|
176
|
+
const isEditing = !!(currentRequest && (EditableChatRequestModel.isEditing(currentRequest)));
|
|
177
|
+
const isPending = () => !!(currentRequest && !isEditing && ChatRequestModel.isInProgress(currentRequest));
|
|
178
|
+
const pending = isPending();
|
|
179
|
+
|
|
157
180
|
return (
|
|
158
181
|
<ChatInput
|
|
182
|
+
branch={this._branch}
|
|
159
183
|
onQuery={this._onQuery.bind(this)}
|
|
160
184
|
onUnpin={this._onUnpin.bind(this)}
|
|
161
185
|
onCancel={this._onCancel.bind(this)}
|
|
@@ -165,13 +189,12 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
165
189
|
onDeleteChangeSetElement={this._onDeleteChangeSetElement.bind(this)}
|
|
166
190
|
onAddContextElement={this.addContextElement.bind(this)}
|
|
167
191
|
onDeleteContextElement={this.deleteContextElement.bind(this)}
|
|
168
|
-
context={this.getContext()}
|
|
169
192
|
onOpenContextElement={this.openContextElement.bind(this)}
|
|
193
|
+
context={this.getContext()}
|
|
170
194
|
chatModel={this._chatModel}
|
|
171
195
|
pinnedAgent={this._pinnedAgent}
|
|
172
196
|
editorProvider={this.editorProvider}
|
|
173
|
-
|
|
174
|
-
resourceUriProvider={this.getResourceUri.bind(this)}
|
|
197
|
+
uri={this.getResourceUri()}
|
|
175
198
|
contextMenuCallback={this.handleContextMenu.bind(this)}
|
|
176
199
|
isEnabled={this.isEnabled}
|
|
177
200
|
setEditorRef={editor => {
|
|
@@ -188,6 +211,14 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
188
211
|
initialValue={this._initialValue}
|
|
189
212
|
openerService={this.openerService}
|
|
190
213
|
suggestions={this._chatModel.suggestions}
|
|
214
|
+
currentRequest={currentRequest}
|
|
215
|
+
isEditing={isEditing}
|
|
216
|
+
pending={pending}
|
|
217
|
+
onResponseChanged={() => {
|
|
218
|
+
if (isPending() !== pending) {
|
|
219
|
+
this.update();
|
|
220
|
+
}
|
|
221
|
+
}}
|
|
191
222
|
/>
|
|
192
223
|
);
|
|
193
224
|
}
|
|
@@ -269,13 +300,14 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
269
300
|
}
|
|
270
301
|
|
|
271
302
|
interface ChatInputProperties {
|
|
303
|
+
branch?: ChatHierarchyBranch;
|
|
272
304
|
onCancel: (requestModel: ChatRequestModel) => void;
|
|
273
305
|
onQuery: (query: string) => void;
|
|
274
306
|
onUnpin: () => void;
|
|
275
307
|
onDragOver: (event: React.DragEvent) => void;
|
|
276
308
|
onDrop: (event: React.DragEvent) => void;
|
|
277
309
|
onDeleteChangeSet: (sessionId: string) => void;
|
|
278
|
-
onDeleteChangeSetElement: (sessionId: string,
|
|
310
|
+
onDeleteChangeSetElement: (sessionId: string, uri: URI) => void;
|
|
279
311
|
onAddContextElement: () => void;
|
|
280
312
|
onDeleteContextElement: (index: number) => void;
|
|
281
313
|
onOpenContextElement: OpenContextElement;
|
|
@@ -284,8 +316,7 @@ interface ChatInputProperties {
|
|
|
284
316
|
chatModel: ChatModel;
|
|
285
317
|
pinnedAgent?: ChatAgent;
|
|
286
318
|
editorProvider: MonacoEditorProvider;
|
|
287
|
-
|
|
288
|
-
resourceUriProvider: () => URI;
|
|
319
|
+
uri: URI;
|
|
289
320
|
contextMenuCallback: (event: IMouseEvent) => void;
|
|
290
321
|
setEditorRef: (editor: SimpleMonacoEditor | undefined) => void;
|
|
291
322
|
showContext?: boolean;
|
|
@@ -297,27 +328,27 @@ interface ChatInputProperties {
|
|
|
297
328
|
decoratorService: ChangeSetDecoratorService;
|
|
298
329
|
initialValue?: string;
|
|
299
330
|
openerService: OpenerService;
|
|
300
|
-
suggestions: readonly ChatSuggestion[]
|
|
331
|
+
suggestions: readonly ChatSuggestion[];
|
|
332
|
+
currentRequest?: ChatRequestModel;
|
|
333
|
+
isEditing: boolean;
|
|
334
|
+
pending: boolean;
|
|
335
|
+
onResponseChanged: () => void;
|
|
301
336
|
}
|
|
302
337
|
|
|
303
338
|
const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInputProperties) => {
|
|
304
339
|
const onDeleteChangeSet = () => props.onDeleteChangeSet(props.chatModel.id);
|
|
305
|
-
const onDeleteChangeSetElement = (
|
|
340
|
+
const onDeleteChangeSetElement = (uri: URI) => props.onDeleteChangeSetElement(props.chatModel.id, uri);
|
|
306
341
|
|
|
307
|
-
const [inProgress, setInProgress] = React.useState(false);
|
|
308
342
|
const [isInputEmpty, setIsInputEmpty] = React.useState(true);
|
|
309
343
|
const [changeSetUI, setChangeSetUI] = React.useState(
|
|
310
|
-
() =>
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
)
|
|
319
|
-
: undefined
|
|
320
|
-
);
|
|
344
|
+
() => buildChangeSetUI(
|
|
345
|
+
props.chatModel.changeSet,
|
|
346
|
+
props.labelProvider,
|
|
347
|
+
props.decoratorService,
|
|
348
|
+
props.actionService.getActionsForChangeset(props.chatModel.changeSet),
|
|
349
|
+
onDeleteChangeSet,
|
|
350
|
+
onDeleteChangeSetElement
|
|
351
|
+
));
|
|
321
352
|
|
|
322
353
|
// eslint-disable-next-line no-null/no-null
|
|
323
354
|
const editorContainerRef = React.useRef<HTMLDivElement | null>(null);
|
|
@@ -326,8 +357,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
326
357
|
const editorRef = React.useRef<SimpleMonacoEditor | undefined>(undefined);
|
|
327
358
|
|
|
328
359
|
React.useEffect(() => {
|
|
329
|
-
const uri = props.
|
|
330
|
-
const resource = props.resources.add(uri, '');
|
|
360
|
+
const uri = props.uri;
|
|
331
361
|
const createInputElement = async () => {
|
|
332
362
|
const paddingTop = 6;
|
|
333
363
|
const lineHeight = 20;
|
|
@@ -403,7 +433,6 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
403
433
|
createInputElement();
|
|
404
434
|
|
|
405
435
|
return () => {
|
|
406
|
-
resource.dispose();
|
|
407
436
|
props.setEditorRef(undefined);
|
|
408
437
|
if (editorRef.current) {
|
|
409
438
|
editorRef.current.dispose();
|
|
@@ -411,55 +440,34 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
411
440
|
};
|
|
412
441
|
}, []);
|
|
413
442
|
|
|
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
443
|
React.useEffect(() => {
|
|
444
|
+
setChangeSetUI(buildChangeSetUI(
|
|
445
|
+
props.chatModel.changeSet,
|
|
446
|
+
props.labelProvider,
|
|
447
|
+
props.decoratorService,
|
|
448
|
+
props.actionService.getActionsForChangeset(props.chatModel.changeSet),
|
|
449
|
+
onDeleteChangeSet,
|
|
450
|
+
onDeleteChangeSetElement
|
|
451
|
+
));
|
|
419
452
|
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
|
-
}
|
|
453
|
+
if (ChatChangeEvent.isChangeSetEvent(event)) {
|
|
454
|
+
setChangeSetUI(buildChangeSetUI(
|
|
455
|
+
props.chatModel.changeSet,
|
|
456
|
+
props.labelProvider,
|
|
457
|
+
props.decoratorService,
|
|
458
|
+
props.actionService.getActionsForChangeset(props.chatModel.changeSet),
|
|
459
|
+
onDeleteChangeSet,
|
|
460
|
+
onDeleteChangeSetElement
|
|
461
|
+
));
|
|
441
462
|
}
|
|
442
463
|
});
|
|
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
464
|
return () => {
|
|
454
|
-
listener
|
|
455
|
-
responseListenerRef.current?.dispose();
|
|
456
|
-
responseListenerRef.current = undefined;
|
|
465
|
+
listener.dispose();
|
|
457
466
|
};
|
|
458
|
-
}, [props.chatModel]);
|
|
467
|
+
}, [props.chatModel, props.labelProvider, props.decoratorService, props.actionService]);
|
|
459
468
|
|
|
460
469
|
React.useEffect(() => {
|
|
461
470
|
const disposable = props.actionService.onDidChange(() => {
|
|
462
|
-
if (!props.chatModel.changeSet) { return; }
|
|
463
471
|
const newActions = props.actionService.getActionsForChangeset(props.chatModel.changeSet);
|
|
464
472
|
setChangeSetUI(current => !current ? current : { ...current, actions: newActions });
|
|
465
473
|
});
|
|
@@ -468,9 +476,6 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
468
476
|
|
|
469
477
|
React.useEffect(() => {
|
|
470
478
|
const disposable = props.decoratorService.onDidChangeDecorations(() => {
|
|
471
|
-
if (!props.chatModel.changeSet) {
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
479
|
setChangeSetUI(buildChangeSetUI(
|
|
475
480
|
props.chatModel.changeSet,
|
|
476
481
|
props.labelProvider,
|
|
@@ -493,7 +498,6 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
493
498
|
if (!value || value.trim().length === 0) {
|
|
494
499
|
return;
|
|
495
500
|
}
|
|
496
|
-
setInProgress(true);
|
|
497
501
|
props.onQuery(value);
|
|
498
502
|
setValue('');
|
|
499
503
|
}, [props.context, props.onQuery, setValue]);
|
|
@@ -571,19 +575,38 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
571
575
|
: []),
|
|
572
576
|
] as Option[];
|
|
573
577
|
|
|
574
|
-
|
|
575
|
-
|
|
578
|
+
let rightOptions: Option[] = [];
|
|
579
|
+
const { currentRequest: latestRequest, isEditing, pending, onResponseChanged } = props;
|
|
580
|
+
React.useEffect(() => {
|
|
581
|
+
if (!latestRequest) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const disposable = latestRequest.response.onDidChange(onResponseChanged);
|
|
585
|
+
return () => disposable.dispose();
|
|
586
|
+
}, [latestRequest, onResponseChanged]);
|
|
587
|
+
if (isEditing) {
|
|
588
|
+
rightOptions = [{
|
|
589
|
+
title: nls.localize('theia/ai/chat-ui/send', 'Send (Enter)'),
|
|
590
|
+
handler: () => {
|
|
591
|
+
if (props.isEnabled) {
|
|
592
|
+
submit(editorRef.current?.document.textEditorModel.getValue() || '');
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
className: 'codicon-send',
|
|
596
|
+
disabled: isInputEmpty || !props.isEnabled
|
|
597
|
+
}];
|
|
598
|
+
} else if (pending) {
|
|
599
|
+
rightOptions = [{
|
|
576
600
|
title: nls.localize('theia/ai/chat-ui/cancel', 'Cancel (Esc)'),
|
|
577
601
|
handler: () => {
|
|
578
|
-
const latestRequest = getLatestRequest(props.chatModel);
|
|
579
602
|
if (latestRequest) {
|
|
580
603
|
props.onCancel(latestRequest);
|
|
581
604
|
}
|
|
582
|
-
setInProgress(false);
|
|
583
605
|
},
|
|
584
606
|
className: 'codicon-stop-circle'
|
|
585
|
-
}]
|
|
586
|
-
|
|
607
|
+
}];
|
|
608
|
+
} else {
|
|
609
|
+
rightOptions = [{
|
|
587
610
|
title: nls.localize('theia/ai/chat-ui/send', 'Send (Enter)'),
|
|
588
611
|
handler: () => {
|
|
589
612
|
if (props.isEnabled) {
|
|
@@ -593,6 +616,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
593
616
|
className: 'codicon-send',
|
|
594
617
|
disabled: isInputEmpty || !props.isEnabled
|
|
595
618
|
}];
|
|
619
|
+
}
|
|
596
620
|
|
|
597
621
|
const contextUI = buildContextUI(props.context, props.labelProvider, props.onDeleteContextElement, props.onOpenContextElement);
|
|
598
622
|
|
|
@@ -624,28 +648,21 @@ const buildChangeSetUI = (
|
|
|
624
648
|
decoratorService: ChangeSetDecoratorService,
|
|
625
649
|
actions: ChangeSetActionRenderer[],
|
|
626
650
|
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
|
-
});
|
|
651
|
+
onDeleteChangeSetElement: (uri: URI) => void
|
|
652
|
+
): ChangeSetUI | undefined => {
|
|
653
|
+
const elements = changeSet.getElements();
|
|
654
|
+
return elements.length ? ({
|
|
655
|
+
title: changeSet.title,
|
|
656
|
+
changeSet,
|
|
657
|
+
deleteChangeSet: onDeleteChangeSet,
|
|
658
|
+
elements: changeSet.getElements().map(element => toUiElement(element, onDeleteChangeSetElement, labelProvider, decoratorService)),
|
|
659
|
+
actions
|
|
660
|
+
}) : undefined;
|
|
661
|
+
};
|
|
646
662
|
|
|
647
663
|
interface ChangeSetUIElement {
|
|
648
664
|
name: string;
|
|
665
|
+
uri: string;
|
|
649
666
|
iconClass: string;
|
|
650
667
|
nameClass: string;
|
|
651
668
|
additionalInfo: string;
|
|
@@ -677,48 +694,70 @@ const ChangeSetBox: React.FunctionComponent<{ changeSet: ChangeSetUI }> = React.
|
|
|
677
694
|
</div>
|
|
678
695
|
<div className='theia-ChatInput-ChangeSet-List'>
|
|
679
696
|
<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
|
-
))}
|
|
697
|
+
{elements.map(element => ChangeSetElement(element))}
|
|
717
698
|
</ul>
|
|
718
699
|
</div>
|
|
719
700
|
</div>
|
|
720
701
|
));
|
|
721
702
|
|
|
703
|
+
function toUiElement(element: ChangeSetElement,
|
|
704
|
+
onDeleteChangeSetElement: (uri: URI) => void,
|
|
705
|
+
labelProvider: LabelProvider,
|
|
706
|
+
decoratorService: ChangeSetDecoratorService
|
|
707
|
+
): ChangeSetUIElement {
|
|
708
|
+
return ({
|
|
709
|
+
open: element.open?.bind(element),
|
|
710
|
+
uri: element.uri.toString(),
|
|
711
|
+
iconClass: element.icon ?? labelProvider.getIcon(element.uri) ?? labelProvider.fileIcon,
|
|
712
|
+
nameClass: `${element.type} ${element.state}`,
|
|
713
|
+
name: element.name ?? labelProvider.getName(element.uri),
|
|
714
|
+
additionalInfo: element.additionalInfo ?? labelProvider.getDetails(element.uri),
|
|
715
|
+
additionalInfoSuffixIcon: decoratorService.getAdditionalInfoSuffixIcon(element),
|
|
716
|
+
openChange: element?.openChange?.bind(element),
|
|
717
|
+
apply: element.state !== 'applied' ? element?.apply?.bind(element) : undefined,
|
|
718
|
+
revert: element.state === 'applied' || element.state === 'stale' ? element?.revert?.bind(element) : undefined,
|
|
719
|
+
delete: () => onDeleteChangeSetElement(element.uri)
|
|
720
|
+
} satisfies ChangeSetUIElement);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const ChangeSetElement: React.FC<ChangeSetUIElement> = element => (
|
|
724
|
+
<li key={element.uri} title={nls.localize('theia/ai/chat-ui/openDiff', 'Open Diff')} onClick={() => element.openChange?.()}>
|
|
725
|
+
<div className={`theia-ChatInput-ChangeSet-Icon ${element.iconClass}`}>
|
|
726
|
+
</div>
|
|
727
|
+
<div className='theia-ChatInput-ChangeSet-labelParts'>
|
|
728
|
+
<span className={`theia-ChatInput-ChangeSet-title ${element.nameClass}`}>
|
|
729
|
+
{element.name}
|
|
730
|
+
</span>
|
|
731
|
+
<div className='theia-ChatInput-ChangeSet-additionalInfo'>
|
|
732
|
+
{element.additionalInfo && <span>{element.additionalInfo}</span>}
|
|
733
|
+
{element.additionalInfoSuffixIcon
|
|
734
|
+
&& <div className={`theia-ChatInput-ChangeSet-AdditionalInfo-SuffixIcon ${element.additionalInfoSuffixIcon.join(' ')}`}></div>}
|
|
735
|
+
</div>
|
|
736
|
+
</div>
|
|
737
|
+
<div className='theia-ChatInput-ChangeSet-Actions'>
|
|
738
|
+
{element.open && (
|
|
739
|
+
<span
|
|
740
|
+
className='codicon codicon-file action'
|
|
741
|
+
title={nls.localize('theia/ai/chat-ui/openOriginalFile', 'Open Original File')}
|
|
742
|
+
onClick={noPropagation(() => element.open!())}
|
|
743
|
+
/>)}
|
|
744
|
+
{element.revert && (
|
|
745
|
+
<span
|
|
746
|
+
className='codicon codicon-discard action'
|
|
747
|
+
title={nls.localizeByDefault('Revert')}
|
|
748
|
+
onClick={noPropagation(() => element.revert!())}
|
|
749
|
+
/>)}
|
|
750
|
+
{element.apply && (
|
|
751
|
+
<span
|
|
752
|
+
className='codicon codicon-check action'
|
|
753
|
+
title={nls.localizeByDefault('Apply')}
|
|
754
|
+
onClick={noPropagation(() => element.apply!())}
|
|
755
|
+
/>)}
|
|
756
|
+
<span className='codicon codicon-close action' title={nls.localizeByDefault('Delete')} onClick={noPropagation(() => element.delete())} />
|
|
757
|
+
</div>
|
|
758
|
+
</li>
|
|
759
|
+
);
|
|
760
|
+
|
|
722
761
|
interface ChatInputOptionsProps {
|
|
723
762
|
leftOptions: Option[];
|
|
724
763
|
rightOptions: Option[];
|
|
@@ -766,11 +805,6 @@ const ChatInputOptions: React.FunctionComponent<ChatInputOptionsProps> = ({ left
|
|
|
766
805
|
</div>
|
|
767
806
|
);
|
|
768
807
|
|
|
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
808
|
function buildContextUI(
|
|
775
809
|
context: readonly AIVariableResolutionRequest[] | undefined,
|
|
776
810
|
labelProvider: LabelProvider,
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import * as React from '@theia/core/shared/react';
|
|
18
|
+
import { nls } from '@theia/core/lib/common/nls';
|
|
19
|
+
import { codicon } from '@theia/core/lib/browser';
|
|
20
|
+
import { ToolCallChatResponseContent } from '@theia/ai-chat/lib/common';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* States the tool confirmation component can be in
|
|
24
|
+
*/
|
|
25
|
+
export type ToolConfirmationState = 'waiting' | 'approved' | 'denied';
|
|
26
|
+
|
|
27
|
+
export interface ToolConfirmationProps {
|
|
28
|
+
response: ToolCallChatResponseContent;
|
|
29
|
+
onApprove: (mode?: 'once' | 'session' | 'forever') => void;
|
|
30
|
+
onDeny: (mode?: 'once' | 'session' | 'forever') => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Component that displays approval/denial buttons for tool execution
|
|
35
|
+
*/
|
|
36
|
+
export const ToolConfirmation: React.FC<ToolConfirmationProps> = ({ response, onApprove, onDeny }) => {
|
|
37
|
+
const [state, setState] = React.useState<ToolConfirmationState>('waiting');
|
|
38
|
+
// Track selected mode for each action
|
|
39
|
+
const [approveMode, setApproveMode] = React.useState<'once' | 'session' | 'forever'>('once');
|
|
40
|
+
const [denyMode, setDenyMode] = React.useState<'once' | 'session' | 'forever'>('once');
|
|
41
|
+
const [dropdownOpen, setDropdownOpen] = React.useState<'approve' | 'deny' | undefined>(undefined);
|
|
42
|
+
|
|
43
|
+
const handleApprove = React.useCallback(() => {
|
|
44
|
+
setState('approved');
|
|
45
|
+
onApprove(approveMode);
|
|
46
|
+
}, [onApprove, approveMode]);
|
|
47
|
+
|
|
48
|
+
const handleDeny = React.useCallback(() => {
|
|
49
|
+
setState('denied');
|
|
50
|
+
onDeny(denyMode);
|
|
51
|
+
}, [onDeny, denyMode]);
|
|
52
|
+
|
|
53
|
+
if (state === 'approved') {
|
|
54
|
+
return (
|
|
55
|
+
<div className="theia-tool-confirmation-status approved">
|
|
56
|
+
<span className={codicon('check')}></span> {nls.localize('theia/ai/chat-ui/toolconfirmation/approved', 'Tool execution approved')}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (state === 'denied') {
|
|
62
|
+
return (
|
|
63
|
+
<div className="theia-tool-confirmation-status denied">
|
|
64
|
+
<span className={codicon('close')}></span> {nls.localize('theia/ai/chat-ui/toolconfirmation/denied', 'Tool execution denied')}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Helper for dropdown options
|
|
70
|
+
const MODES: Array<'once' | 'session' | 'forever'> = ['once', 'session', 'forever'];
|
|
71
|
+
// Unified labels for both main button and dropdown, as requested
|
|
72
|
+
const modeLabel = (type: 'approve' | 'deny', mode: 'once' | 'session' | 'forever') => {
|
|
73
|
+
if (type === 'approve') {
|
|
74
|
+
switch (mode) {
|
|
75
|
+
case 'once': return nls.localize('theia/ai/chat-ui/toolconfirmation/approve', 'Approve');
|
|
76
|
+
case 'session': return nls.localize('theia/ai/chat-ui/toolconfirmation/approve-session', 'Approve for this Chat');
|
|
77
|
+
case 'forever': return nls.localize('theia/ai/chat-ui/toolconfirmation/approve-forever', 'Always Approve');
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
switch (mode) {
|
|
81
|
+
case 'once': return nls.localize('theia/ai/chat-ui/toolconfirmation/deny', 'Deny');
|
|
82
|
+
case 'session': return nls.localize('theia/ai/chat-ui/toolconfirmation/deny-session', 'Deny for this Chat');
|
|
83
|
+
case 'forever': return nls.localize('theia/ai/chat-ui/toolconfirmation/deny-forever', 'Always Deny');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
// Main button label is always the same as the dropdown label for the selected mode
|
|
88
|
+
const mainButtonLabel = modeLabel; // Use the same function for both
|
|
89
|
+
|
|
90
|
+
// Tooltips for dropdown options
|
|
91
|
+
const modeTooltip = (type: 'approve' | 'deny', mode: 'once' | 'session' | 'forever') => {
|
|
92
|
+
if (type === 'approve') {
|
|
93
|
+
switch (mode) {
|
|
94
|
+
case 'once': return nls.localize('theia/ai/chat-ui/toolconfirmation/approve-tooltip', 'Approve this tool call once');
|
|
95
|
+
case 'session': return nls.localize('theia/ai/chat-ui/toolconfirmation/approve-session-tooltip', 'Approve all calls of this tool for this chat session');
|
|
96
|
+
case 'forever': return nls.localize('theia/ai/chat-ui/toolconfirmation/approve-forever-tooltip', 'Always approve this tool');
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
switch (mode) {
|
|
100
|
+
case 'once': return nls.localize('theia/ai/chat-ui/toolconfirmation/deny-tooltip', 'Deny this tool call once');
|
|
101
|
+
case 'session': return nls.localize('theia/ai/chat-ui/toolconfirmation/deny-session-tooltip', 'Deny all calls of this tool for this chat session');
|
|
102
|
+
case 'forever': return nls.localize('theia/ai/chat-ui/toolconfirmation/deny-forever-tooltip', 'Always deny this tool');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Split button for approve/deny
|
|
108
|
+
const renderSplitButton = (type: 'approve' | 'deny') => {
|
|
109
|
+
const selectedMode = type === 'approve' ? approveMode : denyMode;
|
|
110
|
+
const setMode = type === 'approve' ? setApproveMode : setDenyMode;
|
|
111
|
+
const handleMain = type === 'approve' ? handleApprove : handleDeny;
|
|
112
|
+
const otherModes = MODES.filter(m => m !== selectedMode);
|
|
113
|
+
return (
|
|
114
|
+
<div className={`theia-tool-confirmation-split-button ${type}`}
|
|
115
|
+
style={{ display: 'inline-flex', position: 'relative' }}>
|
|
116
|
+
<button
|
|
117
|
+
className={`theia-button ${type === 'approve' ? 'primary' : 'secondary'} theia-tool-confirmation-main-btn`}
|
|
118
|
+
onClick={handleMain}
|
|
119
|
+
>
|
|
120
|
+
{mainButtonLabel(type, selectedMode)}
|
|
121
|
+
</button>
|
|
122
|
+
<button
|
|
123
|
+
className={`theia-button ${type === 'approve' ? 'primary' : 'secondary'} theia-tool-confirmation-chevron-btn`}
|
|
124
|
+
onClick={() => setDropdownOpen(dropdownOpen === type ? undefined : type)}
|
|
125
|
+
aria-haspopup="true"
|
|
126
|
+
aria-expanded={dropdownOpen === type}
|
|
127
|
+
tabIndex={0}
|
|
128
|
+
title={type === 'approve' ? 'More Approve Options' : 'More Deny Options'}
|
|
129
|
+
>
|
|
130
|
+
<span className={codicon('chevron-down')}></span>
|
|
131
|
+
</button>
|
|
132
|
+
{dropdownOpen === type && (
|
|
133
|
+
<ul
|
|
134
|
+
className="theia-tool-confirmation-dropdown-menu"
|
|
135
|
+
onMouseLeave={() => setDropdownOpen(undefined)}
|
|
136
|
+
>
|
|
137
|
+
{otherModes.map(mode => (
|
|
138
|
+
<li
|
|
139
|
+
key={mode}
|
|
140
|
+
className="theia-tool-confirmation-dropdown-item"
|
|
141
|
+
onClick={() => {
|
|
142
|
+
setMode(mode);
|
|
143
|
+
setDropdownOpen(undefined);
|
|
144
|
+
}}
|
|
145
|
+
title={modeTooltip(type, mode)}
|
|
146
|
+
>
|
|
147
|
+
{modeLabel(type, mode)}
|
|
148
|
+
</li>
|
|
149
|
+
))}
|
|
150
|
+
</ul>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div className="theia-tool-confirmation">
|
|
158
|
+
<div className="theia-tool-confirmation-header">
|
|
159
|
+
<span className={codicon('shield')}></span> {nls.localize('theia/ai/chat-ui/toolconfirmation/header', 'Confirm Tool Execution')}
|
|
160
|
+
</div>
|
|
161
|
+
<div className="theia-tool-confirmation-info">
|
|
162
|
+
<div className="theia-tool-confirmation-name">
|
|
163
|
+
<span className="label">{nls.localize('theia/ai/chat-ui/toolconfirmation/tool', 'Tool')}:</span>
|
|
164
|
+
<span className="value">{response.name}</span>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
<div className="theia-tool-confirmation-actions">
|
|
168
|
+
{renderSplitButton('deny')}
|
|
169
|
+
{renderSplitButton('approve')}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
};
|