@theia/ai-chat-ui 1.63.0-next.0 → 1.63.0-next.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/ai-chat-ui-contribution.d.ts +29 -1
- package/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -1
- package/lib/browser/ai-chat-ui-contribution.js +158 -2
- package/lib/browser/ai-chat-ui-contribution.js.map +1 -1
- package/lib/browser/ai-chat-ui-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-chat-ui-frontend-module.js +8 -0
- package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -1
- package/lib/browser/chat-input-widget.d.ts +9 -6
- package/lib/browser/chat-input-widget.d.ts.map +1 -1
- package/lib/browser/chat-input-widget.js +181 -111
- package/lib/browser/chat-input-widget.js.map +1 -1
- package/lib/browser/chat-node-toolbar-action-contribution.d.ts +1 -0
- package/lib/browser/chat-node-toolbar-action-contribution.d.ts.map +1 -1
- package/lib/browser/chat-node-toolbar-action-contribution.js +13 -0
- package/lib/browser/chat-node-toolbar-action-contribution.js.map +1 -1
- package/lib/browser/chat-response-renderer/delegation-response-renderer.d.ts +14 -0
- package/lib/browser/chat-response-renderer/delegation-response-renderer.d.ts.map +1 -0
- package/lib/browser/chat-response-renderer/delegation-response-renderer.js +144 -0
- package/lib/browser/chat-response-renderer/delegation-response-renderer.js.map +1 -0
- package/lib/browser/chat-response-renderer/index.d.ts +2 -0
- package/lib/browser/chat-response-renderer/index.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/index.js +2 -0
- package/lib/browser/chat-response-renderer/index.js.map +1 -1
- package/lib/browser/chat-response-renderer/tool-confirmation.d.ts +17 -0
- package/lib/browser/chat-response-renderer/tool-confirmation.d.ts.map +1 -0
- package/lib/browser/chat-response-renderer/tool-confirmation.js +120 -0
- package/lib/browser/chat-response-renderer/tool-confirmation.js.map +1 -0
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts +5 -1
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +83 -19
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
- package/lib/browser/chat-tree-view/chat-view-tree-input-widget.d.ts +6 -1
- package/lib/browser/chat-tree-view/chat-view-tree-input-widget.d.ts.map +1 -1
- package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js +9 -0
- package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js.map +1 -1
- package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts +8 -0
- package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts.map +1 -1
- package/lib/browser/chat-tree-view/chat-view-tree-widget.js +72 -3
- package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
- package/lib/browser/chat-tree-view/sub-chat-widget.d.ts +22 -0
- package/lib/browser/chat-tree-view/sub-chat-widget.d.ts.map +1 -0
- package/lib/browser/chat-tree-view/sub-chat-widget.js +92 -0
- package/lib/browser/chat-tree-view/sub-chat-widget.js.map +1 -0
- package/lib/browser/chat-view-commands.d.ts +1 -0
- package/lib/browser/chat-view-commands.d.ts.map +1 -1
- package/lib/browser/chat-view-commands.js +5 -0
- package/lib/browser/chat-view-commands.js.map +1 -1
- package/lib/browser/chat-view-contribution.js +2 -1
- package/lib/browser/chat-view-contribution.js.map +1 -1
- package/lib/browser/chat-view-widget.d.ts +6 -3
- package/lib/browser/chat-view-widget.d.ts.map +1 -1
- package/lib/browser/chat-view-widget.js +20 -10
- package/lib/browser/chat-view-widget.js.map +1 -1
- package/package.json +10 -10
- package/src/browser/ai-chat-ui-contribution.ts +166 -5
- package/src/browser/ai-chat-ui-frontend-module.ts +11 -0
- package/src/browser/chat-input-widget.tsx +280 -170
- package/src/browser/chat-node-toolbar-action-contribution.ts +14 -0
- package/src/browser/chat-response-renderer/delegation-response-renderer.tsx +177 -0
- package/src/browser/chat-response-renderer/index.ts +2 -0
- package/src/browser/chat-response-renderer/tool-confirmation.tsx +173 -0
- package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +115 -19
- package/src/browser/chat-tree-view/chat-view-tree-input-widget.tsx +16 -1
- package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +89 -5
- package/src/browser/chat-tree-view/sub-chat-widget.tsx +101 -0
- package/src/browser/chat-view-commands.ts +6 -0
- package/src/browser/chat-view-contribution.ts +1 -1
- package/src/browser/chat-view-widget.tsx +25 -12
- package/src/browser/style/index.css +350 -2
|
@@ -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
|
+
}
|
|
@@ -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' | 'allowed' | 'denied';
|
|
26
|
+
|
|
27
|
+
export interface ToolConfirmationProps {
|
|
28
|
+
response: ToolCallChatResponseContent;
|
|
29
|
+
onAllow: (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, onAllow, onDeny }) => {
|
|
37
|
+
const [state, setState] = React.useState<ToolConfirmationState>('waiting');
|
|
38
|
+
// Track selected mode for each action
|
|
39
|
+
const [allowMode, setAllowMode] = React.useState<'once' | 'session' | 'forever'>('once');
|
|
40
|
+
const [denyMode, setDenyMode] = React.useState<'once' | 'session' | 'forever'>('once');
|
|
41
|
+
const [dropdownOpen, setDropdownOpen] = React.useState<'allow' | 'deny' | undefined>(undefined);
|
|
42
|
+
|
|
43
|
+
const handleAllow = React.useCallback(() => {
|
|
44
|
+
setState('allowed');
|
|
45
|
+
onAllow(allowMode);
|
|
46
|
+
}, [onAllow, allowMode]);
|
|
47
|
+
|
|
48
|
+
const handleDeny = React.useCallback(() => {
|
|
49
|
+
setState('denied');
|
|
50
|
+
onDeny(denyMode);
|
|
51
|
+
}, [onDeny, denyMode]);
|
|
52
|
+
|
|
53
|
+
if (state === 'allowed') {
|
|
54
|
+
return (
|
|
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
|
+
</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: 'allow' | 'deny', mode: 'once' | 'session' | 'forever') => {
|
|
73
|
+
if (type === 'allow') {
|
|
74
|
+
switch (mode) {
|
|
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
|
+
}
|
|
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: 'allow' | 'deny', mode: 'once' | 'session' | 'forever') => {
|
|
92
|
+
if (type === 'allow') {
|
|
93
|
+
switch (mode) {
|
|
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
|
+
}
|
|
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: 'allow' | 'deny') => {
|
|
109
|
+
const selectedMode = type === 'allow' ? allowMode : denyMode;
|
|
110
|
+
const setMode = type === 'allow' ? setAllowMode : setDenyMode;
|
|
111
|
+
const handleMain = type === 'allow' ? handleAllow : 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 === 'allow' ? 'primary' : 'secondary'} theia-tool-confirmation-main-btn`}
|
|
118
|
+
onClick={handleMain}
|
|
119
|
+
>
|
|
120
|
+
{mainButtonLabel(type, selectedMode)}
|
|
121
|
+
</button>
|
|
122
|
+
<button
|
|
123
|
+
className={`theia-button ${type === 'allow' ? '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 === 'allow' ? 'More Allow 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('allow')}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
};
|
|
@@ -15,15 +15,22 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
|
|
18
|
-
import { injectable } from '@theia/core/shared/inversify';
|
|
18
|
+
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
19
19
|
import { ChatResponseContent, ToolCallChatResponseContent } from '@theia/ai-chat/lib/common';
|
|
20
20
|
import { ReactNode } from '@theia/core/shared/react';
|
|
21
21
|
import { nls } from '@theia/core/lib/common/nls';
|
|
22
|
+
import { codicon } from '@theia/core/lib/browser';
|
|
22
23
|
import * as React from '@theia/core/shared/react';
|
|
24
|
+
import { ToolConfirmation, ToolConfirmationState } from './tool-confirmation';
|
|
25
|
+
import { ToolConfirmationManager, ToolConfirmationMode } from '@theia/ai-chat/lib/browser/chat-tool-preferences';
|
|
26
|
+
import { ResponseNode } from '../chat-tree-view';
|
|
23
27
|
|
|
24
28
|
@injectable()
|
|
25
29
|
export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallChatResponseContent> {
|
|
26
30
|
|
|
31
|
+
@inject(ToolConfirmationManager)
|
|
32
|
+
protected toolConfirmationManager: ToolConfirmationManager;
|
|
33
|
+
|
|
27
34
|
canHandle(response: ChatResponseContent): number {
|
|
28
35
|
if (ToolCallChatResponseContent.is(response)) {
|
|
29
36
|
return 10;
|
|
@@ -31,23 +38,20 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
|
|
|
31
38
|
return -1;
|
|
32
39
|
}
|
|
33
40
|
|
|
34
|
-
render(response: ToolCallChatResponseContent): ReactNode {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
)}
|
|
49
|
-
</h4>
|
|
50
|
-
);
|
|
41
|
+
render(response: ToolCallChatResponseContent, parentNode: ResponseNode): ReactNode {
|
|
42
|
+
const chatId = parentNode.sessionId;
|
|
43
|
+
const confirmationMode = response.name ? this.getToolConfirmationSettings(response.name, chatId) : ToolConfirmationMode.DISABLED;
|
|
44
|
+
return <ToolCallContent
|
|
45
|
+
response={response}
|
|
46
|
+
confirmationMode={confirmationMode}
|
|
47
|
+
toolConfirmationManager={this.toolConfirmationManager}
|
|
48
|
+
chatId={chatId}
|
|
49
|
+
renderCollapsibleArguments={this.renderCollapsibleArguments.bind(this)}
|
|
50
|
+
tryPrettyPrintJson={this.tryPrettyPrintJson.bind(this)} />;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
protected getToolConfirmationSettings(responseId: string, chatId: string): ToolConfirmationMode {
|
|
54
|
+
return this.toolConfirmationManager.getConfirmationMode(responseId, chatId);
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
protected renderCollapsibleArguments(args: string | undefined): ReactNode {
|
|
@@ -97,5 +101,97 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
|
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
const Spinner = () => (
|
|
100
|
-
<
|
|
104
|
+
<span className={`${codicon('loading')} theia-animation-spin`}></span>
|
|
101
105
|
);
|
|
106
|
+
|
|
107
|
+
interface ToolCallContentProps {
|
|
108
|
+
response: ToolCallChatResponseContent;
|
|
109
|
+
confirmationMode: ToolConfirmationMode;
|
|
110
|
+
toolConfirmationManager: ToolConfirmationManager;
|
|
111
|
+
chatId: string;
|
|
112
|
+
renderCollapsibleArguments: (args: string | undefined) => ReactNode;
|
|
113
|
+
tryPrettyPrintJson: (response: ToolCallChatResponseContent) => string | undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A function component to handle tool call rendering and confirmation
|
|
118
|
+
*/
|
|
119
|
+
const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmationMode, toolConfirmationManager, chatId, tryPrettyPrintJson, renderCollapsibleArguments }) => {
|
|
120
|
+
const [confirmationState, setConfirmationState] = React.useState<ToolConfirmationState>('waiting');
|
|
121
|
+
|
|
122
|
+
React.useEffect(() => {
|
|
123
|
+
if (confirmationMode === ToolConfirmationMode.ALWAYS_ALLOW) {
|
|
124
|
+
response.confirm();
|
|
125
|
+
setConfirmationState('allowed');
|
|
126
|
+
return;
|
|
127
|
+
} else if (confirmationMode === ToolConfirmationMode.DISABLED) {
|
|
128
|
+
response.deny();
|
|
129
|
+
setConfirmationState('denied');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
response.confirmed.then(
|
|
133
|
+
confirmed => {
|
|
134
|
+
if (confirmed === true) {
|
|
135
|
+
setConfirmationState('allowed');
|
|
136
|
+
} else {
|
|
137
|
+
setConfirmationState('denied');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
.catch(() => {
|
|
142
|
+
setConfirmationState('denied');
|
|
143
|
+
});
|
|
144
|
+
}, [response, confirmationMode]);
|
|
145
|
+
|
|
146
|
+
const handleAllow = React.useCallback((mode: 'once' | 'session' | 'forever' = 'once') => {
|
|
147
|
+
if (mode === 'forever' && response.name) {
|
|
148
|
+
toolConfirmationManager.setConfirmationMode(response.name, ToolConfirmationMode.ALWAYS_ALLOW);
|
|
149
|
+
} else if (mode === 'session' && response.name) {
|
|
150
|
+
toolConfirmationManager.setSessionConfirmationMode(response.name, ToolConfirmationMode.ALWAYS_ALLOW, chatId);
|
|
151
|
+
}
|
|
152
|
+
response.confirm();
|
|
153
|
+
}, [response, toolConfirmationManager, chatId]);
|
|
154
|
+
|
|
155
|
+
const handleDeny = React.useCallback((mode: 'once' | 'session' | 'forever' = 'once') => {
|
|
156
|
+
if (mode === 'forever' && response.name) {
|
|
157
|
+
toolConfirmationManager.setConfirmationMode(response.name, ToolConfirmationMode.DISABLED);
|
|
158
|
+
} else if (mode === 'session' && response.name) {
|
|
159
|
+
toolConfirmationManager.setSessionConfirmationMode(response.name, ToolConfirmationMode.DISABLED, chatId);
|
|
160
|
+
}
|
|
161
|
+
response.deny();
|
|
162
|
+
}, [response, toolConfirmationManager, chatId]);
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<div className='theia-toolCall'>
|
|
166
|
+
<h4>
|
|
167
|
+
{confirmationState === 'denied' ? (
|
|
168
|
+
<span className="theia-tool-denied">
|
|
169
|
+
<span className={codicon('error')}></span> {nls.localize('theia/ai/chat-ui/toolcall-part-renderer/denied', 'Execution denied')}: {response.name}
|
|
170
|
+
</span>
|
|
171
|
+
) : response.finished ? (
|
|
172
|
+
<details>
|
|
173
|
+
<summary>{nls.localize('theia/ai/chat-ui/toolcall-part-renderer/finished', 'Ran')} {response.name}
|
|
174
|
+
({renderCollapsibleArguments(response.arguments)})
|
|
175
|
+
</summary>
|
|
176
|
+
<pre>{tryPrettyPrintJson(response)}</pre>
|
|
177
|
+
</details>
|
|
178
|
+
) : (
|
|
179
|
+
confirmationState === 'allowed' && (
|
|
180
|
+
<span>
|
|
181
|
+
<Spinner /> {nls.localizeByDefault('Running')} {response.name}
|
|
182
|
+
</span>
|
|
183
|
+
)
|
|
184
|
+
)}
|
|
185
|
+
</h4>
|
|
186
|
+
|
|
187
|
+
{/* Show confirmation UI when waiting for allow */}
|
|
188
|
+
{confirmationState === 'waiting' && (
|
|
189
|
+
<ToolConfirmation
|
|
190
|
+
response={response}
|
|
191
|
+
onAllow={handleAllow}
|
|
192
|
+
onDeny={handleDeny}
|
|
193
|
+
/>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
};
|
|
@@ -19,7 +19,7 @@ import { AIChatInputWidget, type AIChatInputConfiguration } from '../chat-input-
|
|
|
19
19
|
import type { EditableRequestNode } from './chat-view-tree-widget';
|
|
20
20
|
import { URI } from '@theia/core';
|
|
21
21
|
import { CHAT_VIEW_LANGUAGE_EXTENSION } from '../chat-view-language-contribution';
|
|
22
|
-
import type { ChatRequestModel, EditableChatRequestModel } from '@theia/ai-chat';
|
|
22
|
+
import type { ChatRequestModel, EditableChatRequestModel, ChatHierarchyBranch } from '@theia/ai-chat';
|
|
23
23
|
import type { AIVariableResolutionRequest } from '@theia/ai-core';
|
|
24
24
|
import { Key } from '@theia/core/lib/browser';
|
|
25
25
|
|
|
@@ -29,6 +29,10 @@ export interface AIChatTreeInputConfiguration extends AIChatInputConfiguration {
|
|
|
29
29
|
export const AIChatTreeInputArgs = Symbol('AIChatTreeInputArgs');
|
|
30
30
|
export interface AIChatTreeInputArgs {
|
|
31
31
|
node: EditableRequestNode;
|
|
32
|
+
/**
|
|
33
|
+
* The branch of the chat tree for this request node (used by the input widget for state tracking).
|
|
34
|
+
*/
|
|
35
|
+
branch?: ChatHierarchyBranch;
|
|
32
36
|
initialValue?: string;
|
|
33
37
|
onQuery: (query: string) => Promise<void>;
|
|
34
38
|
onUnpin?: () => void;
|
|
@@ -60,6 +64,13 @@ export class AIChatTreeInputWidget extends AIChatInputWidget {
|
|
|
60
64
|
@postConstruct()
|
|
61
65
|
protected override init(): void {
|
|
62
66
|
super.init();
|
|
67
|
+
this.updateBranch();
|
|
68
|
+
|
|
69
|
+
const request = this.requestNode.request;
|
|
70
|
+
this.toDispose.push(request.session.onDidChange(() => {
|
|
71
|
+
this.updateBranch();
|
|
72
|
+
}));
|
|
73
|
+
|
|
63
74
|
this.addKeyListener(this.node, Key.ESCAPE, () => {
|
|
64
75
|
this.request.cancelEdit();
|
|
65
76
|
});
|
|
@@ -71,6 +82,10 @@ export class AIChatTreeInputWidget extends AIChatInputWidget {
|
|
|
71
82
|
});
|
|
72
83
|
}
|
|
73
84
|
|
|
85
|
+
protected updateBranch(): void {
|
|
86
|
+
this.branch = this.args.branch ?? this.requestNode.branch;
|
|
87
|
+
}
|
|
88
|
+
|
|
74
89
|
protected override getResourceUri(): URI {
|
|
75
90
|
return new URI(`ai-chat:/${this.requestNode.id}-input.${CHAT_VIEW_LANGUAGE_EXTENSION}`);
|
|
76
91
|
}
|