@theia/ai-chat-ui 1.63.0-next.24 → 1.63.0-next.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/ai-chat-ui-contribution.d.ts +29 -1
- package/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -1
- package/lib/browser/ai-chat-ui-contribution.js +156 -0
- 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 +7 -6
- package/lib/browser/chat-input-widget.d.ts.map +1 -1
- package/lib/browser/chat-input-widget.js +81 -22
- 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.js +9 -9
- 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.js +2 -1
- package/lib/browser/chat-view-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 +10 -10
- package/src/browser/ai-chat-ui-contribution.ts +164 -3
- package/src/browser/ai-chat-ui-frontend-module.ts +11 -0
- package/src/browser/chat-input-widget.tsx +111 -35
- 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 +10 -10
- 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 +1 -1
- package/src/browser/chat-view-widget.tsx +2 -5
- package/src/browser/style/index.css +178 -3
|
@@ -14,24 +14,25 @@
|
|
|
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 { ChangeSetDecoratorService } from '@theia/ai-chat/lib/browser/change-set-decorator-service';
|
|
21
|
+
import { ImageContextVariable } from '@theia/ai-chat/lib/common/image-context-variable';
|
|
22
|
+
import { AIVariableResolutionRequest } from '@theia/ai-core';
|
|
23
|
+
import { FrontendVariableService } from '@theia/ai-core/lib/browser';
|
|
20
24
|
import { DisposableCollection, 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
29
|
import { IMouseEvent } from '@theia/monaco-editor-core';
|
|
26
|
-
import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
|
|
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';
|
|
35
36
|
|
|
36
37
|
type Query = (query: string) => Promise<void>;
|
|
37
38
|
type Unpin = () => void;
|
|
@@ -185,6 +186,7 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
185
186
|
onCancel={this._onCancel.bind(this)}
|
|
186
187
|
onDragOver={this.onDragOver.bind(this)}
|
|
187
188
|
onDrop={this.onDrop.bind(this)}
|
|
189
|
+
onPaste={this.onPaste.bind(this)}
|
|
188
190
|
onDeleteChangeSet={this._onDeleteChangeSet.bind(this)}
|
|
189
191
|
onDeleteChangeSetElement={this._onDeleteChangeSetElement.bind(this)}
|
|
190
192
|
onAddContextElement={this.addContextElement.bind(this)}
|
|
@@ -257,6 +259,26 @@ export class AIChatInputWidget extends ReactWidget {
|
|
|
257
259
|
});
|
|
258
260
|
}
|
|
259
261
|
|
|
262
|
+
protected onPaste(event: ClipboardEvent): void {
|
|
263
|
+
this.variableService.getPasteResult(event, { type: 'ai-chat-input-widget' }).then(result => {
|
|
264
|
+
result.variables.forEach(variable => this.addContext(variable));
|
|
265
|
+
if (result.text) {
|
|
266
|
+
const position = this.editorRef?.getControl().getPosition();
|
|
267
|
+
if (position && result.text) {
|
|
268
|
+
this.editorRef?.getControl().executeEdits('paste', [{
|
|
269
|
+
range: {
|
|
270
|
+
startLineNumber: position.lineNumber,
|
|
271
|
+
startColumn: position.column,
|
|
272
|
+
endLineNumber: position.lineNumber,
|
|
273
|
+
endColumn: position.column
|
|
274
|
+
},
|
|
275
|
+
text: result.text
|
|
276
|
+
}]);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
260
282
|
protected async openContextElement(request: AIVariableResolutionRequest): Promise<void> {
|
|
261
283
|
const session = this.chatService.getSessions().find(candidate => candidate.model.id === this._chatModel.id);
|
|
262
284
|
const context = { session };
|
|
@@ -306,6 +328,7 @@ interface ChatInputProperties {
|
|
|
306
328
|
onUnpin: () => void;
|
|
307
329
|
onDragOver: (event: React.DragEvent) => void;
|
|
308
330
|
onDrop: (event: React.DragEvent) => void;
|
|
331
|
+
onPaste: (event: ClipboardEvent) => void;
|
|
309
332
|
onDeleteChangeSet: (sessionId: string) => void;
|
|
310
333
|
onDeleteChangeSetElement: (sessionId: string, uri: URI) => void;
|
|
311
334
|
onAddContextElement: () => void;
|
|
@@ -355,6 +378,25 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
355
378
|
// eslint-disable-next-line no-null/no-null
|
|
356
379
|
const placeholderRef = React.useRef<HTMLDivElement | null>(null);
|
|
357
380
|
const editorRef = React.useRef<SimpleMonacoEditor | undefined>(undefined);
|
|
381
|
+
// eslint-disable-next-line no-null/no-null
|
|
382
|
+
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
383
|
+
|
|
384
|
+
// Handle paste events on the container
|
|
385
|
+
const handlePaste = React.useCallback((event: ClipboardEvent) => {
|
|
386
|
+
props.onPaste(event);
|
|
387
|
+
}, [props.onPaste]);
|
|
388
|
+
|
|
389
|
+
// Set up paste handler on the container div
|
|
390
|
+
React.useEffect(() => {
|
|
391
|
+
const container = containerRef.current;
|
|
392
|
+
if (container) {
|
|
393
|
+
container.addEventListener('paste', handlePaste, true);
|
|
394
|
+
return () => {
|
|
395
|
+
container.removeEventListener('paste', handlePaste, true);
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
return undefined;
|
|
399
|
+
}, [handlePaste]);
|
|
358
400
|
|
|
359
401
|
React.useEffect(() => {
|
|
360
402
|
const uri = props.uri;
|
|
@@ -472,7 +514,7 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
472
514
|
setChangeSetUI(current => !current ? current : { ...current, actions: newActions });
|
|
473
515
|
});
|
|
474
516
|
return () => disposable.dispose();
|
|
475
|
-
});
|
|
517
|
+
}, [props.actionService, props.chatModel.changeSet]);
|
|
476
518
|
|
|
477
519
|
React.useEffect(() => {
|
|
478
520
|
const disposable = props.decoratorService.onDidChangeDecorations(() => {
|
|
@@ -498,8 +540,13 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
498
540
|
if (!value || value.trim().length === 0) {
|
|
499
541
|
return;
|
|
500
542
|
}
|
|
543
|
+
|
|
501
544
|
props.onQuery(value);
|
|
502
545
|
setValue('');
|
|
546
|
+
|
|
547
|
+
if (editorRef.current) {
|
|
548
|
+
editorRef.current.document.textEditorModel.setValue('');
|
|
549
|
+
}
|
|
503
550
|
}, [props.context, props.onQuery, setValue]);
|
|
504
551
|
|
|
505
552
|
const onKeyDown = React.useCallback((event: React.KeyboardEvent) => {
|
|
@@ -620,21 +667,23 @@ const ChatInput: React.FunctionComponent<ChatInputProperties> = (props: ChatInpu
|
|
|
620
667
|
|
|
621
668
|
const contextUI = buildContextUI(props.context, props.labelProvider, props.onDeleteContextElement, props.onOpenContextElement);
|
|
622
669
|
|
|
623
|
-
return
|
|
624
|
-
|
|
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} />
|
|
670
|
+
return (
|
|
671
|
+
<div className='theia-ChatInput' onDragOver={props.onDragOver} onDrop={props.onDrop} ref={containerRef}>
|
|
672
|
+
{props.showSuggestions !== false && <ChatInputAgentSuggestions suggestions={props.suggestions} opener={props.openerService} />}
|
|
673
|
+
{props.showChangeSet && changeSetUI?.elements &&
|
|
674
|
+
<ChangeSetBox changeSet={changeSetUI} />
|
|
634
675
|
}
|
|
635
|
-
<
|
|
676
|
+
<div className='theia-ChatInput-Editor-Box'>
|
|
677
|
+
<div className='theia-ChatInput-Editor' ref={editorContainerRef} onKeyDown={onKeyDown} onFocus={handleInputFocus} onBlur={handleInputBlur}>
|
|
678
|
+
<div ref={placeholderRef} className='theia-ChatInput-Editor-Placeholder'>{nls.localizeByDefault('Ask a question')}</div>
|
|
679
|
+
</div>
|
|
680
|
+
{props.context && props.context.length > 0 &&
|
|
681
|
+
<ChatContext context={contextUI.context} />
|
|
682
|
+
}
|
|
683
|
+
<ChatInputOptions leftOptions={leftOptions} rightOptions={rightOptions} />
|
|
684
|
+
</div>
|
|
636
685
|
</div>
|
|
637
|
-
|
|
686
|
+
);
|
|
638
687
|
};
|
|
639
688
|
|
|
640
689
|
const noPropagation = (handler: () => void) => (e: React.MouseEvent) => {
|
|
@@ -816,6 +865,7 @@ function buildContextUI(
|
|
|
816
865
|
}
|
|
817
866
|
return {
|
|
818
867
|
context: context.map((element, index) => ({
|
|
868
|
+
variable: element,
|
|
819
869
|
name: labelProvider.getName(element),
|
|
820
870
|
iconClass: labelProvider.getIcon(element),
|
|
821
871
|
nameClass: element.variable.name,
|
|
@@ -829,6 +879,7 @@ function buildContextUI(
|
|
|
829
879
|
|
|
830
880
|
interface ChatContextUI {
|
|
831
881
|
context: {
|
|
882
|
+
variable: AIVariableResolutionRequest,
|
|
832
883
|
name: string;
|
|
833
884
|
iconClass: string;
|
|
834
885
|
nameClass: string;
|
|
@@ -842,20 +893,45 @@ interface ChatContextUI {
|
|
|
842
893
|
const ChatContext: React.FunctionComponent<ChatContextUI> = ({ context }) => (
|
|
843
894
|
<div className="theia-ChatInput-ChatContext">
|
|
844
895
|
<ul>
|
|
845
|
-
{context.map((element, index) =>
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
<
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
896
|
+
{context.map((element, index) => {
|
|
897
|
+
if (ImageContextVariable.isImageContextRequest(element.variable)) {
|
|
898
|
+
const variable = ImageContextVariable.parseRequest(element.variable)!;
|
|
899
|
+
return <li key={index} className="theia-ChatInput-ChatContext-Element theia-ChatInput-ImageContext-Element"
|
|
900
|
+
title={variable.name ?? variable.wsRelativePath} onClick={() => element.open?.()}>
|
|
901
|
+
<div className="theia-ChatInput-ChatContext-Row">
|
|
902
|
+
<div className={`theia-ChatInput-ChatContext-Icon ${element.iconClass}`} />
|
|
903
|
+
<div className="theia-ChatInput-ChatContext-labelParts">
|
|
904
|
+
<span className={`theia-ChatInput-ChatContext-title ${element.nameClass}`}>
|
|
905
|
+
{variable.name ?? variable.wsRelativePath?.split('/').pop()}
|
|
906
|
+
</span>
|
|
907
|
+
<span className='theia-ChatInput-ChatContext-additionalInfo'>
|
|
908
|
+
{element.additionalInfo}
|
|
909
|
+
</span>
|
|
910
|
+
</div>
|
|
911
|
+
<span className="codicon codicon-close action" title={nls.localizeByDefault('Delete')} onClick={e => { e.stopPropagation(); element.delete(); }} />
|
|
912
|
+
</div>
|
|
913
|
+
<div className="theia-ChatInput-ChatContext-ImageRow">
|
|
914
|
+
<div className='theia-ChatInput-ImagePreview-Item'>
|
|
915
|
+
<img src={`data:${variable.mimeType};base64,${variable.data}`} alt={variable.name} />
|
|
916
|
+
</div>
|
|
917
|
+
</div>
|
|
918
|
+
</li>;
|
|
919
|
+
}
|
|
920
|
+
return <li key={index} className="theia-ChatInput-ChatContext-Element" title={element.details} onClick={() => element.open?.()}>
|
|
921
|
+
<div className="theia-ChatInput-ChatContext-Row">
|
|
922
|
+
<div className={`theia-ChatInput-ChatContext-Icon ${element.iconClass}`} />
|
|
923
|
+
<div className="theia-ChatInput-ChatContext-labelParts">
|
|
924
|
+
<span className={`theia-ChatInput-ChatContext-title ${element.nameClass}`}>
|
|
925
|
+
{element.name}
|
|
926
|
+
</span>
|
|
927
|
+
<span className='theia-ChatInput-ChatContext-additionalInfo'>
|
|
928
|
+
{element.additionalInfo}
|
|
929
|
+
</span>
|
|
930
|
+
</div>
|
|
931
|
+
<span className="codicon codicon-close action" title={nls.localizeByDefault('Delete')} onClick={e => { e.stopPropagation(); element.delete(); }} />
|
|
855
932
|
</div>
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
))}
|
|
933
|
+
</li>;
|
|
934
|
+
})}
|
|
859
935
|
</ul>
|
|
860
936
|
</div>
|
|
861
937
|
);
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,177 @@
|
|
|
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
|
+
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
17
|
+
import { ChatRequestInvocation, ChatResponseContent, ChatResponseModel } from '@theia/ai-chat';
|
|
18
|
+
import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
|
|
19
|
+
import * as React from '@theia/core/shared/react';
|
|
20
|
+
import { DelegationResponseContent, isDelegationResponseContent } from '@theia/ai-chat/lib/browser/delegation-response-content';
|
|
21
|
+
import { ResponseNode } from '../chat-tree-view';
|
|
22
|
+
import { CompositeTreeNode } from '@theia/core/lib/browser';
|
|
23
|
+
import { SubChatWidgetFactory } from '../chat-tree-view/sub-chat-widget';
|
|
24
|
+
import { DisposableCollection } from '@theia/core';
|
|
25
|
+
|
|
26
|
+
@injectable()
|
|
27
|
+
export class DelegationResponseRenderer implements ChatResponsePartRenderer<DelegationResponseContent> {
|
|
28
|
+
|
|
29
|
+
@inject(SubChatWidgetFactory)
|
|
30
|
+
subChatWidgetFactory: SubChatWidgetFactory;
|
|
31
|
+
|
|
32
|
+
canHandle(response: ChatResponseContent): number {
|
|
33
|
+
if (isDelegationResponseContent(response)) {
|
|
34
|
+
return 10;
|
|
35
|
+
}
|
|
36
|
+
return -1;
|
|
37
|
+
}
|
|
38
|
+
render(response: DelegationResponseContent, parentNode: ResponseNode): React.ReactNode {
|
|
39
|
+
return this.renderExpandableNode(response, parentNode);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private renderExpandableNode(response: DelegationResponseContent, parentNode: ResponseNode): React.ReactNode {
|
|
43
|
+
return <DelegatedChat
|
|
44
|
+
response={response.response}
|
|
45
|
+
agentId={response.agentId}
|
|
46
|
+
prompt={response.prompt}
|
|
47
|
+
parentNode={parentNode}
|
|
48
|
+
subChatWidgetFactory={this.subChatWidgetFactory} />;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface DelegatedChatProps {
|
|
53
|
+
response: ChatRequestInvocation;
|
|
54
|
+
agentId: string;
|
|
55
|
+
prompt: string;
|
|
56
|
+
parentNode: ResponseNode;
|
|
57
|
+
subChatWidgetFactory: SubChatWidgetFactory;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface DelegatedChatState {
|
|
61
|
+
node?: ResponseNode;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class DelegatedChat extends React.Component<DelegatedChatProps, DelegatedChatState> {
|
|
65
|
+
private widget: ReturnType<SubChatWidgetFactory>;
|
|
66
|
+
private readonly toDispose = new DisposableCollection();
|
|
67
|
+
|
|
68
|
+
constructor(props: DelegatedChatProps) {
|
|
69
|
+
super(props);
|
|
70
|
+
this.state = {
|
|
71
|
+
node: undefined
|
|
72
|
+
};
|
|
73
|
+
this.widget = props.subChatWidgetFactory();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
override componentDidMount(): void {
|
|
77
|
+
// Start rendering as soon as the response is created (streaming mode)
|
|
78
|
+
this.props.response.responseCreated.then(chatModel => {
|
|
79
|
+
const node = mapResponseToNode(chatModel, this.props.parentNode);
|
|
80
|
+
this.setState({ node });
|
|
81
|
+
|
|
82
|
+
// Listen for changes to update the rendering as the response streams in
|
|
83
|
+
const changeListener = () => {
|
|
84
|
+
// Force re-render when the response content changes
|
|
85
|
+
this.forceUpdate();
|
|
86
|
+
};
|
|
87
|
+
this.toDispose.push(chatModel.onDidChange(changeListener));
|
|
88
|
+
}).catch(error => {
|
|
89
|
+
console.error('Failed to create delegated chat response:', error);
|
|
90
|
+
// Still try to handle completion in case of partial success
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Keep the completion handling for final cleanup if needed
|
|
94
|
+
this.props.response.responseCompleted.then(() => {
|
|
95
|
+
// Final update when response is complete
|
|
96
|
+
this.forceUpdate();
|
|
97
|
+
}).catch(error => {
|
|
98
|
+
console.error('Error in delegated chat response completion:', error);
|
|
99
|
+
// Force update anyway to show any partial content or error state
|
|
100
|
+
this.forceUpdate();
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override componentWillUnmount(): void {
|
|
105
|
+
this.toDispose.dispose();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
override render(): React.ReactNode {
|
|
109
|
+
const { agentId, prompt } = this.props;
|
|
110
|
+
const hasNode = !!this.state.node;
|
|
111
|
+
const isComplete = this.state.node?.response.isComplete ?? false;
|
|
112
|
+
const isCanceled = this.state.node?.response.isCanceled ?? false;
|
|
113
|
+
const isError = this.state.node?.response.isError ?? false;
|
|
114
|
+
|
|
115
|
+
let statusIcon = '';
|
|
116
|
+
let statusText = '';
|
|
117
|
+
if (hasNode) {
|
|
118
|
+
if (isComplete) {
|
|
119
|
+
statusIcon = 'codicon-check';
|
|
120
|
+
statusText = 'completed';
|
|
121
|
+
} else if (isCanceled) {
|
|
122
|
+
statusIcon = 'codicon-cancel';
|
|
123
|
+
statusText = 'canceled';
|
|
124
|
+
} else if (isError) {
|
|
125
|
+
statusIcon = 'codicon-error';
|
|
126
|
+
statusText = 'error';
|
|
127
|
+
} else {
|
|
128
|
+
statusIcon = 'codicon-loading';
|
|
129
|
+
statusText = 'generating...';
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
statusIcon = 'codicon-loading';
|
|
133
|
+
statusText = 'starting...';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div className="theia-delegation-container">
|
|
138
|
+
<details className="delegation-response-details">
|
|
139
|
+
<summary className="delegation-summary">
|
|
140
|
+
<div className="delegation-header">
|
|
141
|
+
<span className="delegation-agent">
|
|
142
|
+
<strong>Agent:</strong> {agentId}
|
|
143
|
+
</span>
|
|
144
|
+
<span className="delegation-status">
|
|
145
|
+
<span className={`codicon ${statusIcon} delegation-status-icon`}></span>
|
|
146
|
+
<span className="delegation-status-text">{statusText}</span>
|
|
147
|
+
</span>
|
|
148
|
+
</div>
|
|
149
|
+
</summary>
|
|
150
|
+
<div className="delegation-content">
|
|
151
|
+
<div className="delegation-prompt-section">
|
|
152
|
+
<strong>Delegated prompt:</strong>
|
|
153
|
+
<div className="delegation-prompt">{prompt}</div>
|
|
154
|
+
</div>
|
|
155
|
+
<div className="delegation-response-section">
|
|
156
|
+
<strong>Response:</strong>
|
|
157
|
+
<div className='delegation-response-placeholder'>
|
|
158
|
+
{hasNode && this.state.node ? this.widget.renderChatResponse(this.state.node) :
|
|
159
|
+
<div className="theia-ChatContentInProgress">Starting delegation...</div>
|
|
160
|
+
}
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</details>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function mapResponseToNode(response: ChatResponseModel, parentNode: ResponseNode): ResponseNode {
|
|
171
|
+
return {
|
|
172
|
+
id: response.id,
|
|
173
|
+
parent: parentNode as unknown as CompositeTreeNode,
|
|
174
|
+
response,
|
|
175
|
+
sessionId: parentNode.sessionId
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -22,38 +22,38 @@ import { ToolCallChatResponseContent } from '@theia/ai-chat/lib/common';
|
|
|
22
22
|
/**
|
|
23
23
|
* States the tool confirmation component can be in
|
|
24
24
|
*/
|
|
25
|
-
export type ToolConfirmationState = 'waiting' | '
|
|
25
|
+
export type ToolConfirmationState = 'waiting' | 'allowed' | 'denied';
|
|
26
26
|
|
|
27
27
|
export interface ToolConfirmationProps {
|
|
28
28
|
response: ToolCallChatResponseContent;
|
|
29
|
-
|
|
29
|
+
onAllow: (mode?: 'once' | 'session' | 'forever') => void;
|
|
30
30
|
onDeny: (mode?: 'once' | 'session' | 'forever') => void;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Component that displays approval/denial buttons for tool execution
|
|
35
35
|
*/
|
|
36
|
-
export const ToolConfirmation: React.FC<ToolConfirmationProps> = ({ response,
|
|
36
|
+
export const ToolConfirmation: React.FC<ToolConfirmationProps> = ({ response, onAllow, onDeny }) => {
|
|
37
37
|
const [state, setState] = React.useState<ToolConfirmationState>('waiting');
|
|
38
38
|
// Track selected mode for each action
|
|
39
|
-
const [
|
|
39
|
+
const [allowMode, setAllowMode] = React.useState<'once' | 'session' | 'forever'>('once');
|
|
40
40
|
const [denyMode, setDenyMode] = React.useState<'once' | 'session' | 'forever'>('once');
|
|
41
|
-
const [dropdownOpen, setDropdownOpen] = React.useState<'
|
|
41
|
+
const [dropdownOpen, setDropdownOpen] = React.useState<'allow' | 'deny' | undefined>(undefined);
|
|
42
42
|
|
|
43
|
-
const
|
|
44
|
-
setState('
|
|
45
|
-
|
|
46
|
-
}, [
|
|
43
|
+
const handleAllow = React.useCallback(() => {
|
|
44
|
+
setState('allowed');
|
|
45
|
+
onAllow(allowMode);
|
|
46
|
+
}, [onAllow, allowMode]);
|
|
47
47
|
|
|
48
48
|
const handleDeny = React.useCallback(() => {
|
|
49
49
|
setState('denied');
|
|
50
50
|
onDeny(denyMode);
|
|
51
51
|
}, [onDeny, denyMode]);
|
|
52
52
|
|
|
53
|
-
if (state === '
|
|
53
|
+
if (state === 'allowed') {
|
|
54
54
|
return (
|
|
55
|
-
<div className="theia-tool-confirmation-status
|
|
56
|
-
<span className={codicon('check')}></span> {nls.localize('theia/ai/chat-ui/toolconfirmation/
|
|
55
|
+
<div className="theia-tool-confirmation-status allowed">
|
|
56
|
+
<span className={codicon('check')}></span> {nls.localize('theia/ai/chat-ui/toolconfirmation/allowed', 'Tool execution allowed')}
|
|
57
57
|
</div>
|
|
58
58
|
);
|
|
59
59
|
}
|
|
@@ -69,12 +69,12 @@ export const ToolConfirmation: React.FC<ToolConfirmationProps> = ({ response, on
|
|
|
69
69
|
// Helper for dropdown options
|
|
70
70
|
const MODES: Array<'once' | 'session' | 'forever'> = ['once', 'session', 'forever'];
|
|
71
71
|
// Unified labels for both main button and dropdown, as requested
|
|
72
|
-
const modeLabel = (type: '
|
|
73
|
-
if (type === '
|
|
72
|
+
const modeLabel = (type: 'allow' | 'deny', mode: 'once' | 'session' | 'forever') => {
|
|
73
|
+
if (type === 'allow') {
|
|
74
74
|
switch (mode) {
|
|
75
|
-
case 'once': return nls.localize('theia/ai/chat-ui/toolconfirmation/
|
|
76
|
-
case 'session': return nls.localize('theia/ai/chat-ui/toolconfirmation/
|
|
77
|
-
case 'forever': return nls.localize('theia/ai/chat-ui/toolconfirmation/
|
|
75
|
+
case 'once': return nls.localize('theia/ai/chat-ui/toolconfirmation/allow', 'Allow');
|
|
76
|
+
case 'session': return nls.localize('theia/ai/chat-ui/toolconfirmation/allow-session', 'Allow for this Chat');
|
|
77
|
+
case 'forever': return nls.localize('theia/ai/chat-ui/toolconfirmation/allow-forever', 'Always Allow');
|
|
78
78
|
}
|
|
79
79
|
} else {
|
|
80
80
|
switch (mode) {
|
|
@@ -88,12 +88,12 @@ export const ToolConfirmation: React.FC<ToolConfirmationProps> = ({ response, on
|
|
|
88
88
|
const mainButtonLabel = modeLabel; // Use the same function for both
|
|
89
89
|
|
|
90
90
|
// Tooltips for dropdown options
|
|
91
|
-
const modeTooltip = (type: '
|
|
92
|
-
if (type === '
|
|
91
|
+
const modeTooltip = (type: 'allow' | 'deny', mode: 'once' | 'session' | 'forever') => {
|
|
92
|
+
if (type === 'allow') {
|
|
93
93
|
switch (mode) {
|
|
94
|
-
case 'once': return nls.localize('theia/ai/chat-ui/toolconfirmation/
|
|
95
|
-
case 'session': return nls.localize('theia/ai/chat-ui/toolconfirmation/
|
|
96
|
-
case 'forever': return nls.localize('theia/ai/chat-ui/toolconfirmation/
|
|
94
|
+
case 'once': return nls.localize('theia/ai/chat-ui/toolconfirmation/allow-tooltip', 'Allow this tool call once');
|
|
95
|
+
case 'session': return nls.localize('theia/ai/chat-ui/toolconfirmation/allow-session-tooltip', 'Allow all calls of this tool for this chat session');
|
|
96
|
+
case 'forever': return nls.localize('theia/ai/chat-ui/toolconfirmation/allow-forever-tooltip', 'Always allow this tool');
|
|
97
97
|
}
|
|
98
98
|
} else {
|
|
99
99
|
switch (mode) {
|
|
@@ -105,27 +105,27 @@ export const ToolConfirmation: React.FC<ToolConfirmationProps> = ({ response, on
|
|
|
105
105
|
};
|
|
106
106
|
|
|
107
107
|
// Split button for approve/deny
|
|
108
|
-
const renderSplitButton = (type: '
|
|
109
|
-
const selectedMode = type === '
|
|
110
|
-
const setMode = type === '
|
|
111
|
-
const handleMain = type === '
|
|
108
|
+
const renderSplitButton = (type: 'allow' | 'deny') => {
|
|
109
|
+
const selectedMode = type === 'allow' ? allowMode : denyMode;
|
|
110
|
+
const setMode = type === 'allow' ? setAllowMode : setDenyMode;
|
|
111
|
+
const handleMain = type === 'allow' ? handleAllow : handleDeny;
|
|
112
112
|
const otherModes = MODES.filter(m => m !== selectedMode);
|
|
113
113
|
return (
|
|
114
114
|
<div className={`theia-tool-confirmation-split-button ${type}`}
|
|
115
115
|
style={{ display: 'inline-flex', position: 'relative' }}>
|
|
116
116
|
<button
|
|
117
|
-
className={`theia-button ${type === '
|
|
117
|
+
className={`theia-button ${type === 'allow' ? 'primary' : 'secondary'} theia-tool-confirmation-main-btn`}
|
|
118
118
|
onClick={handleMain}
|
|
119
119
|
>
|
|
120
120
|
{mainButtonLabel(type, selectedMode)}
|
|
121
121
|
</button>
|
|
122
122
|
<button
|
|
123
|
-
className={`theia-button ${type === '
|
|
123
|
+
className={`theia-button ${type === 'allow' ? 'primary' : 'secondary'} theia-tool-confirmation-chevron-btn`}
|
|
124
124
|
onClick={() => setDropdownOpen(dropdownOpen === type ? undefined : type)}
|
|
125
125
|
aria-haspopup="true"
|
|
126
126
|
aria-expanded={dropdownOpen === type}
|
|
127
127
|
tabIndex={0}
|
|
128
|
-
title={type === '
|
|
128
|
+
title={type === 'allow' ? 'More Allow Options' : 'More Deny Options'}
|
|
129
129
|
>
|
|
130
130
|
<span className={codicon('chevron-down')}></span>
|
|
131
131
|
</button>
|
|
@@ -166,7 +166,7 @@ export const ToolConfirmation: React.FC<ToolConfirmationProps> = ({ response, on
|
|
|
166
166
|
</div>
|
|
167
167
|
<div className="theia-tool-confirmation-actions">
|
|
168
168
|
{renderSplitButton('deny')}
|
|
169
|
-
{renderSplitButton('
|
|
169
|
+
{renderSplitButton('allow')}
|
|
170
170
|
</div>
|
|
171
171
|
</div>
|
|
172
172
|
);
|
|
@@ -101,7 +101,7 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
const Spinner = () => (
|
|
104
|
-
<span className={codicon('loading')}></span>
|
|
104
|
+
<span className={`${codicon('loading')} theia-animation-spin`}></span>
|
|
105
105
|
);
|
|
106
106
|
|
|
107
107
|
interface ToolCallContentProps {
|
|
@@ -120,9 +120,9 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmatio
|
|
|
120
120
|
const [confirmationState, setConfirmationState] = React.useState<ToolConfirmationState>('waiting');
|
|
121
121
|
|
|
122
122
|
React.useEffect(() => {
|
|
123
|
-
if (confirmationMode === ToolConfirmationMode.
|
|
123
|
+
if (confirmationMode === ToolConfirmationMode.ALWAYS_ALLOW) {
|
|
124
124
|
response.confirm();
|
|
125
|
-
setConfirmationState('
|
|
125
|
+
setConfirmationState('allowed');
|
|
126
126
|
return;
|
|
127
127
|
} else if (confirmationMode === ToolConfirmationMode.DISABLED) {
|
|
128
128
|
response.deny();
|
|
@@ -132,7 +132,7 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmatio
|
|
|
132
132
|
response.confirmed.then(
|
|
133
133
|
confirmed => {
|
|
134
134
|
if (confirmed === true) {
|
|
135
|
-
setConfirmationState('
|
|
135
|
+
setConfirmationState('allowed');
|
|
136
136
|
} else {
|
|
137
137
|
setConfirmationState('denied');
|
|
138
138
|
}
|
|
@@ -143,11 +143,11 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmatio
|
|
|
143
143
|
});
|
|
144
144
|
}, [response, confirmationMode]);
|
|
145
145
|
|
|
146
|
-
const
|
|
146
|
+
const handleAllow = React.useCallback((mode: 'once' | 'session' | 'forever' = 'once') => {
|
|
147
147
|
if (mode === 'forever' && response.name) {
|
|
148
|
-
toolConfirmationManager.setConfirmationMode(response.name, ToolConfirmationMode.
|
|
148
|
+
toolConfirmationManager.setConfirmationMode(response.name, ToolConfirmationMode.ALWAYS_ALLOW);
|
|
149
149
|
} else if (mode === 'session' && response.name) {
|
|
150
|
-
toolConfirmationManager.setSessionConfirmationMode(response.name, ToolConfirmationMode.
|
|
150
|
+
toolConfirmationManager.setSessionConfirmationMode(response.name, ToolConfirmationMode.ALWAYS_ALLOW, chatId);
|
|
151
151
|
}
|
|
152
152
|
response.confirm();
|
|
153
153
|
}, [response, toolConfirmationManager, chatId]);
|
|
@@ -176,7 +176,7 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmatio
|
|
|
176
176
|
<pre>{tryPrettyPrintJson(response)}</pre>
|
|
177
177
|
</details>
|
|
178
178
|
) : (
|
|
179
|
-
confirmationState === '
|
|
179
|
+
confirmationState === 'allowed' && (
|
|
180
180
|
<span>
|
|
181
181
|
<Spinner /> {nls.localizeByDefault('Running')} {response.name}
|
|
182
182
|
</span>
|
|
@@ -184,11 +184,11 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmatio
|
|
|
184
184
|
)}
|
|
185
185
|
</h4>
|
|
186
186
|
|
|
187
|
-
{/* Show confirmation UI when waiting for
|
|
187
|
+
{/* Show confirmation UI when waiting for allow */}
|
|
188
188
|
{confirmationState === 'waiting' && (
|
|
189
189
|
<ToolConfirmation
|
|
190
190
|
response={response}
|
|
191
|
-
|
|
191
|
+
onAllow={handleAllow}
|
|
192
192
|
onDeny={handleDeny}
|
|
193
193
|
/>
|
|
194
194
|
)}
|