@theia/ai-chat-ui 1.63.0-next.24 → 1.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/ai-chat-ui-contribution.d.ts +31 -1
- package/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -1
- package/lib/browser/ai-chat-ui-contribution.js +192 -8
- package/lib/browser/ai-chat-ui-contribution.js.map +1 -1
- package/lib/browser/ai-chat-ui-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-chat-ui-frontend-module.js +8 -0
- package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -1
- package/lib/browser/chat-input-widget.d.ts +24 -16
- package/lib/browser/chat-input-widget.d.ts.map +1 -1
- package/lib/browser/chat-input-widget.js +199 -37
- package/lib/browser/chat-input-widget.js.map +1 -1
- package/lib/browser/chat-node-toolbar-action-contribution.d.ts +1 -0
- package/lib/browser/chat-node-toolbar-action-contribution.d.ts.map +1 -1
- package/lib/browser/chat-node-toolbar-action-contribution.js +13 -0
- package/lib/browser/chat-node-toolbar-action-contribution.js.map +1 -1
- package/lib/browser/chat-response-renderer/delegation-response-renderer.d.ts +14 -0
- package/lib/browser/chat-response-renderer/delegation-response-renderer.d.ts.map +1 -0
- package/lib/browser/chat-response-renderer/delegation-response-renderer.js +144 -0
- package/lib/browser/chat-response-renderer/delegation-response-renderer.js.map +1 -0
- package/lib/browser/chat-response-renderer/index.d.ts +1 -0
- package/lib/browser/chat-response-renderer/index.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/index.js +1 -0
- package/lib/browser/chat-response-renderer/index.js.map +1 -1
- package/lib/browser/chat-response-renderer/tool-confirmation.d.ts +2 -2
- package/lib/browser/chat-response-renderer/tool-confirmation.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/tool-confirmation.js +23 -23
- package/lib/browser/chat-response-renderer/tool-confirmation.js.map +1 -1
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts +4 -1
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -1
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +65 -32
- package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
- package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts +3 -1
- package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts.map +1 -1
- package/lib/browser/chat-tree-view/chat-view-tree-widget.js +50 -8
- package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
- package/lib/browser/chat-tree-view/sub-chat-widget.d.ts +22 -0
- package/lib/browser/chat-tree-view/sub-chat-widget.d.ts.map +1 -0
- package/lib/browser/chat-tree-view/sub-chat-widget.js +92 -0
- package/lib/browser/chat-tree-view/sub-chat-widget.js.map +1 -0
- package/lib/browser/chat-view-commands.d.ts +1 -0
- package/lib/browser/chat-view-commands.d.ts.map +1 -1
- package/lib/browser/chat-view-commands.js +5 -0
- package/lib/browser/chat-view-commands.js.map +1 -1
- package/lib/browser/chat-view-contribution.d.ts +2 -0
- package/lib/browser/chat-view-contribution.d.ts.map +1 -1
- package/lib/browser/chat-view-contribution.js +31 -18
- package/lib/browser/chat-view-contribution.js.map +1 -1
- package/lib/browser/chat-view-widget-toolbar-contribution.d.ts +2 -0
- package/lib/browser/chat-view-widget-toolbar-contribution.d.ts.map +1 -1
- package/lib/browser/chat-view-widget-toolbar-contribution.js +13 -5
- package/lib/browser/chat-view-widget-toolbar-contribution.js.map +1 -1
- package/lib/browser/chat-view-widget.d.ts +1 -1
- package/lib/browser/chat-view-widget.d.ts.map +1 -1
- package/lib/browser/chat-view-widget.js +1 -4
- package/lib/browser/chat-view-widget.js.map +1 -1
- package/package.json +11 -11
- package/src/browser/ai-chat-ui-contribution.ts +191 -12
- package/src/browser/ai-chat-ui-frontend-module.ts +11 -0
- package/src/browser/chat-input-widget.tsx +253 -58
- package/src/browser/chat-node-toolbar-action-contribution.ts +14 -0
- package/src/browser/chat-response-renderer/delegation-response-renderer.tsx +177 -0
- package/src/browser/chat-response-renderer/index.ts +1 -0
- package/src/browser/chat-response-renderer/tool-confirmation.tsx +30 -30
- package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +95 -60
- package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +58 -8
- package/src/browser/chat-tree-view/sub-chat-widget.tsx +101 -0
- package/src/browser/chat-view-commands.ts +6 -0
- package/src/browser/chat-view-contribution.ts +29 -18
- package/src/browser/chat-view-widget-toolbar-contribution.tsx +12 -5
- package/src/browser/chat-view-widget.tsx +2 -5
- package/src/browser/style/index.css +209 -5
|
@@ -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
|
);
|
|
@@ -19,11 +19,13 @@ import { inject, injectable } from '@theia/core/shared/inversify';
|
|
|
19
19
|
import { ChatResponseContent, ToolCallChatResponseContent } from '@theia/ai-chat/lib/common';
|
|
20
20
|
import { ReactNode } from '@theia/core/shared/react';
|
|
21
21
|
import { nls } from '@theia/core/lib/common/nls';
|
|
22
|
-
import { codicon } from '@theia/core/lib/browser';
|
|
22
|
+
import { codicon, OpenerService } from '@theia/core/lib/browser';
|
|
23
23
|
import * as React from '@theia/core/shared/react';
|
|
24
24
|
import { ToolConfirmation, ToolConfirmationState } from './tool-confirmation';
|
|
25
25
|
import { ToolConfirmationManager, ToolConfirmationMode } from '@theia/ai-chat/lib/browser/chat-tool-preferences';
|
|
26
26
|
import { ResponseNode } from '../chat-tree-view';
|
|
27
|
+
import { useMarkdownRendering } from './markdown-part-renderer';
|
|
28
|
+
import { ToolCallResult } from '@theia/ai-core';
|
|
27
29
|
|
|
28
30
|
@injectable()
|
|
29
31
|
export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallChatResponseContent> {
|
|
@@ -31,6 +33,9 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
|
|
|
31
33
|
@inject(ToolConfirmationManager)
|
|
32
34
|
protected toolConfirmationManager: ToolConfirmationManager;
|
|
33
35
|
|
|
36
|
+
@inject(OpenerService)
|
|
37
|
+
protected openerService: OpenerService;
|
|
38
|
+
|
|
34
39
|
canHandle(response: ChatResponseContent): number {
|
|
35
40
|
if (ToolCallChatResponseContent.is(response)) {
|
|
36
41
|
return 10;
|
|
@@ -47,7 +52,52 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
|
|
|
47
52
|
toolConfirmationManager={this.toolConfirmationManager}
|
|
48
53
|
chatId={chatId}
|
|
49
54
|
renderCollapsibleArguments={this.renderCollapsibleArguments.bind(this)}
|
|
50
|
-
|
|
55
|
+
responseRenderer={this.renderResult.bind(this)} />;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
protected renderResult(response: ToolCallChatResponseContent): ReactNode {
|
|
59
|
+
const result = this.tryParse(response.result);
|
|
60
|
+
if (!result) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
if (typeof result === 'string') {
|
|
64
|
+
return <pre>{JSON.stringify(result, undefined, 2)}</pre>;
|
|
65
|
+
}
|
|
66
|
+
if ('content' in result) {
|
|
67
|
+
return <div className='theia-toolCall-response-content'>
|
|
68
|
+
{result.content.map((content, idx) => {
|
|
69
|
+
switch (content.type) {
|
|
70
|
+
case 'image': {
|
|
71
|
+
return <div key={`content-${idx}-${content.type}`} className='theia-toolCall-image-result'>
|
|
72
|
+
<img src={`data:${content.mimeType};base64,${content.base64data}`} />
|
|
73
|
+
</div>;
|
|
74
|
+
}
|
|
75
|
+
case 'text': {
|
|
76
|
+
return <div key={`content-${idx}-${content.type}`} className='theia-toolCall-text-result'>
|
|
77
|
+
<MarkdownRender text={content.text} openerService={this.openerService} />
|
|
78
|
+
</div>;
|
|
79
|
+
}
|
|
80
|
+
case 'audio':
|
|
81
|
+
case 'error':
|
|
82
|
+
default: {
|
|
83
|
+
return <div key={`content-${idx}-${content.type}`} className='theia-toolCall-default-result'><pre>{JSON.stringify(response, undefined, 2)}</pre></div>;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
})}
|
|
87
|
+
</div>;
|
|
88
|
+
}
|
|
89
|
+
return <pre>{JSON.stringify(result, undefined, 2)}</pre>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private tryParse(result: ToolCallResult): ToolCallResult {
|
|
93
|
+
if (!result) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
return typeof result === 'string' ? JSON.parse(result) : result;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
51
101
|
}
|
|
52
102
|
|
|
53
103
|
protected getToolConfirmationSettings(responseId: string, chatId: string): ToolConfirmationMode {
|
|
@@ -75,33 +125,10 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
|
|
|
75
125
|
return args;
|
|
76
126
|
}
|
|
77
127
|
}
|
|
78
|
-
|
|
79
|
-
private tryPrettyPrintJson(response: ToolCallChatResponseContent): string | undefined {
|
|
80
|
-
let responseContent = response.result;
|
|
81
|
-
try {
|
|
82
|
-
if (responseContent) {
|
|
83
|
-
if (typeof responseContent === 'string') {
|
|
84
|
-
responseContent = JSON.parse(responseContent);
|
|
85
|
-
}
|
|
86
|
-
responseContent = JSON.stringify(responseContent, undefined, 2);
|
|
87
|
-
}
|
|
88
|
-
} catch (e) {
|
|
89
|
-
if (typeof responseContent !== 'string') {
|
|
90
|
-
responseContent = nls.localize(
|
|
91
|
-
'theia/ai/chat-ui/toolcall-part-renderer/prettyPrintError',
|
|
92
|
-
"The content could not be converted to string: '{0}'. This is the original content: '{1}'.",
|
|
93
|
-
e.message,
|
|
94
|
-
responseContent
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
// fall through
|
|
98
|
-
}
|
|
99
|
-
return responseContent;
|
|
100
|
-
}
|
|
101
128
|
}
|
|
102
129
|
|
|
103
130
|
const Spinner = () => (
|
|
104
|
-
<span className={codicon('loading')}></span>
|
|
131
|
+
<span className={`${codicon('loading')} theia-animation-spin`}></span>
|
|
105
132
|
);
|
|
106
133
|
|
|
107
134
|
interface ToolCallContentProps {
|
|
@@ -110,19 +137,19 @@ interface ToolCallContentProps {
|
|
|
110
137
|
toolConfirmationManager: ToolConfirmationManager;
|
|
111
138
|
chatId: string;
|
|
112
139
|
renderCollapsibleArguments: (args: string | undefined) => ReactNode;
|
|
113
|
-
|
|
140
|
+
responseRenderer: (response: ToolCallChatResponseContent) => ReactNode | undefined;
|
|
114
141
|
}
|
|
115
142
|
|
|
116
143
|
/**
|
|
117
144
|
* A function component to handle tool call rendering and confirmation
|
|
118
145
|
*/
|
|
119
|
-
const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmationMode, toolConfirmationManager, chatId,
|
|
146
|
+
const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmationMode, toolConfirmationManager, chatId, responseRenderer, renderCollapsibleArguments }) => {
|
|
120
147
|
const [confirmationState, setConfirmationState] = React.useState<ToolConfirmationState>('waiting');
|
|
121
148
|
|
|
122
149
|
React.useEffect(() => {
|
|
123
|
-
if (confirmationMode === ToolConfirmationMode.
|
|
150
|
+
if (confirmationMode === ToolConfirmationMode.ALWAYS_ALLOW) {
|
|
124
151
|
response.confirm();
|
|
125
|
-
setConfirmationState('
|
|
152
|
+
setConfirmationState('allowed');
|
|
126
153
|
return;
|
|
127
154
|
} else if (confirmationMode === ToolConfirmationMode.DISABLED) {
|
|
128
155
|
response.deny();
|
|
@@ -132,7 +159,7 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmatio
|
|
|
132
159
|
response.confirmed.then(
|
|
133
160
|
confirmed => {
|
|
134
161
|
if (confirmed === true) {
|
|
135
|
-
setConfirmationState('
|
|
162
|
+
setConfirmationState('allowed');
|
|
136
163
|
} else {
|
|
137
164
|
setConfirmationState('denied');
|
|
138
165
|
}
|
|
@@ -143,11 +170,11 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmatio
|
|
|
143
170
|
});
|
|
144
171
|
}, [response, confirmationMode]);
|
|
145
172
|
|
|
146
|
-
const
|
|
173
|
+
const handleAllow = React.useCallback((mode: 'once' | 'session' | 'forever' = 'once') => {
|
|
147
174
|
if (mode === 'forever' && response.name) {
|
|
148
|
-
toolConfirmationManager.setConfirmationMode(response.name, ToolConfirmationMode.
|
|
175
|
+
toolConfirmationManager.setConfirmationMode(response.name, ToolConfirmationMode.ALWAYS_ALLOW);
|
|
149
176
|
} else if (mode === 'session' && response.name) {
|
|
150
|
-
toolConfirmationManager.setSessionConfirmationMode(response.name, ToolConfirmationMode.
|
|
177
|
+
toolConfirmationManager.setSessionConfirmationMode(response.name, ToolConfirmationMode.ALWAYS_ALLOW, chatId);
|
|
151
178
|
}
|
|
152
179
|
response.confirm();
|
|
153
180
|
}, [response, toolConfirmationManager, chatId]);
|
|
@@ -163,35 +190,43 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({ response, confirmatio
|
|
|
163
190
|
|
|
164
191
|
return (
|
|
165
192
|
<div className='theia-toolCall'>
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
<span className=
|
|
169
|
-
|
|
193
|
+
{confirmationState === 'denied' ? (
|
|
194
|
+
<span className='theia-toolCall-denied'>
|
|
195
|
+
<span className={codicon('error')}></span> {nls.localize('theia/ai/chat-ui/toolcall-part-renderer/denied', 'Execution denied')}: {response.name}
|
|
196
|
+
</span>
|
|
197
|
+
) : response.finished ? (
|
|
198
|
+
<details className='theia-toolCall-finished'>
|
|
199
|
+
<summary>
|
|
200
|
+
{nls.localize('theia/ai/chat-ui/toolcall-part-renderer/finished', 'Ran')} {response.name}
|
|
201
|
+
({renderCollapsibleArguments(response.arguments)})
|
|
202
|
+
</summary>
|
|
203
|
+
<div className='theia-toolCall-response-result'>
|
|
204
|
+
{responseRenderer(response)}
|
|
205
|
+
</div>
|
|
206
|
+
</details>
|
|
207
|
+
) : (
|
|
208
|
+
confirmationState === 'allowed' && (
|
|
209
|
+
<span className='theia-toolCall-allowed'>
|
|
210
|
+
<Spinner /> {nls.localizeByDefault('Running')} {response.name}
|
|
170
211
|
</span>
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
</summary>
|
|
176
|
-
<pre>{tryPrettyPrintJson(response)}</pre>
|
|
177
|
-
</details>
|
|
178
|
-
) : (
|
|
179
|
-
confirmationState === 'approved' && (
|
|
180
|
-
<span>
|
|
181
|
-
<Spinner /> {nls.localizeByDefault('Running')} {response.name}
|
|
182
|
-
</span>
|
|
183
|
-
)
|
|
184
|
-
)}
|
|
185
|
-
</h4>
|
|
186
|
-
|
|
187
|
-
{/* Show confirmation UI when waiting for approval */}
|
|
212
|
+
)
|
|
213
|
+
)}
|
|
214
|
+
|
|
215
|
+
{/* Show confirmation UI when waiting for allow */}
|
|
188
216
|
{confirmationState === 'waiting' && (
|
|
189
|
-
<
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
217
|
+
<span className='theia-toolCall-waiting'>
|
|
218
|
+
<ToolConfirmation
|
|
219
|
+
response={response}
|
|
220
|
+
onAllow={handleAllow}
|
|
221
|
+
onDeny={handleDeny}
|
|
222
|
+
/>
|
|
223
|
+
</span>
|
|
194
224
|
)}
|
|
195
225
|
</div>
|
|
196
226
|
);
|
|
197
227
|
};
|
|
228
|
+
|
|
229
|
+
const MarkdownRender = ({ text, openerService }: { text: string; openerService: OpenerService }) => {
|
|
230
|
+
const ref = useMarkdownRendering(text, openerService);
|
|
231
|
+
return <div ref={ref}></div>;
|
|
232
|
+
};
|
|
@@ -142,6 +142,8 @@ export class ChatViewTreeWidget extends TreeWidget {
|
|
|
142
142
|
|
|
143
143
|
onScrollLockChange?: (temporaryLocked: boolean) => void;
|
|
144
144
|
|
|
145
|
+
protected lastScrollTop = 0;
|
|
146
|
+
|
|
145
147
|
set shouldScrollToEnd(shouldScrollToEnd: boolean) {
|
|
146
148
|
this._shouldScrollToEnd = shouldScrollToEnd;
|
|
147
149
|
this.shouldScrollToRow = this._shouldScrollToEnd;
|
|
@@ -189,6 +191,9 @@ export class ChatViewTreeWidget extends TreeWidget {
|
|
|
189
191
|
this.handleScrollEvent(scrollEvent);
|
|
190
192
|
})
|
|
191
193
|
]);
|
|
194
|
+
|
|
195
|
+
// Initialize lastScrollTop with current scroll position
|
|
196
|
+
this.lastScrollTop = this.getCurrentScrollTop(undefined);
|
|
192
197
|
}
|
|
193
198
|
|
|
194
199
|
public setEnabled(enabled: boolean): void {
|
|
@@ -196,20 +201,39 @@ export class ChatViewTreeWidget extends TreeWidget {
|
|
|
196
201
|
this.update();
|
|
197
202
|
}
|
|
198
203
|
|
|
199
|
-
protected handleScrollEvent(
|
|
200
|
-
//
|
|
204
|
+
protected handleScrollEvent(scrollEvent: unknown): void {
|
|
205
|
+
// Get current scroll position
|
|
206
|
+
const currentScrollTop = this.getCurrentScrollTop(scrollEvent);
|
|
201
207
|
const isAtBottom = this.isScrolledToBottom();
|
|
202
208
|
|
|
203
|
-
//
|
|
209
|
+
// Determine scroll direction
|
|
210
|
+
const isScrollingUp = currentScrollTop < this.lastScrollTop;
|
|
211
|
+
const isScrollingDown = currentScrollTop > this.lastScrollTop;
|
|
212
|
+
|
|
213
|
+
// Handle scroll lock logic based on direction and position
|
|
214
|
+
// The key insight is that we only enable temporary lock when scrolling UP,
|
|
215
|
+
// and only disable it when scrolling DOWN to the bottom. This prevents
|
|
216
|
+
// the jitter when users try to scroll up by just a few pixels from the bottom.
|
|
204
217
|
if (this.shouldScrollToEnd) {
|
|
205
|
-
if
|
|
206
|
-
|
|
218
|
+
// Auto-scroll is enabled, check if we need to enable temporary lock
|
|
219
|
+
if (isScrollingUp) {
|
|
220
|
+
// User is scrolling up and not at bottom - enable temporary lock
|
|
207
221
|
this.setTemporaryScrollLock(true);
|
|
208
222
|
}
|
|
209
|
-
|
|
210
|
-
//
|
|
211
|
-
|
|
223
|
+
// Note: We don't disable temporary lock when scrolling down while shouldScrollToEnd is true
|
|
224
|
+
// because that would cause jitter. The lock will be disabled when user reaches bottom.
|
|
225
|
+
} else {
|
|
226
|
+
// Temporary lock is active, check if we should disable it
|
|
227
|
+
if (isScrollingDown && isAtBottom) {
|
|
228
|
+
// User scrolled back to bottom - disable temporary lock
|
|
229
|
+
this.setTemporaryScrollLock(false);
|
|
230
|
+
}
|
|
231
|
+
// Note: We don't change the lock state when scrolling up while locked,
|
|
232
|
+
// as the user is intentionally scrolling away from auto-scroll behavior.
|
|
212
233
|
}
|
|
234
|
+
|
|
235
|
+
// Update last scroll position for next comparison
|
|
236
|
+
this.lastScrollTop = currentScrollTop;
|
|
213
237
|
}
|
|
214
238
|
|
|
215
239
|
protected setTemporaryScrollLock(enabled: boolean): void {
|
|
@@ -217,6 +241,32 @@ export class ChatViewTreeWidget extends TreeWidget {
|
|
|
217
241
|
this.onScrollLockChange?.(enabled);
|
|
218
242
|
}
|
|
219
243
|
|
|
244
|
+
protected getCurrentScrollTop(scrollEvent: unknown): number {
|
|
245
|
+
// For virtualized trees, try to get scroll position from the virtualized view
|
|
246
|
+
if (this.props.virtualized !== false && this.view) {
|
|
247
|
+
const scrollState = this.getVirtualizedScrollState();
|
|
248
|
+
if (scrollState !== undefined) {
|
|
249
|
+
return scrollState.scrollTop;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Try to extract scroll position from the scroll event
|
|
254
|
+
if (scrollEvent && typeof scrollEvent === 'object' && 'scrollTop' in scrollEvent) {
|
|
255
|
+
const scrollEventWithScrollTop = scrollEvent as { scrollTop: unknown };
|
|
256
|
+
const scrollTop = scrollEventWithScrollTop.scrollTop;
|
|
257
|
+
if (typeof scrollTop === 'number' && !isNaN(scrollTop)) {
|
|
258
|
+
return scrollTop;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Last resort: use DOM scroll position
|
|
263
|
+
if (this.node && typeof this.node.scrollTop === 'number') {
|
|
264
|
+
return this.node.scrollTop;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
220
270
|
protected override renderTree(model: TreeModel): React.ReactNode {
|
|
221
271
|
if (!this.isEnabled) {
|
|
222
272
|
return this.renderDisabledMessage();
|