@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.
Files changed (69) hide show
  1. package/lib/browser/ai-chat-ui-contribution.d.ts +29 -1
  2. package/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -1
  3. package/lib/browser/ai-chat-ui-contribution.js +158 -2
  4. package/lib/browser/ai-chat-ui-contribution.js.map +1 -1
  5. package/lib/browser/ai-chat-ui-frontend-module.d.ts.map +1 -1
  6. package/lib/browser/ai-chat-ui-frontend-module.js +8 -0
  7. package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -1
  8. package/lib/browser/chat-input-widget.d.ts +9 -6
  9. package/lib/browser/chat-input-widget.d.ts.map +1 -1
  10. package/lib/browser/chat-input-widget.js +181 -111
  11. package/lib/browser/chat-input-widget.js.map +1 -1
  12. package/lib/browser/chat-node-toolbar-action-contribution.d.ts +1 -0
  13. package/lib/browser/chat-node-toolbar-action-contribution.d.ts.map +1 -1
  14. package/lib/browser/chat-node-toolbar-action-contribution.js +13 -0
  15. package/lib/browser/chat-node-toolbar-action-contribution.js.map +1 -1
  16. package/lib/browser/chat-response-renderer/delegation-response-renderer.d.ts +14 -0
  17. package/lib/browser/chat-response-renderer/delegation-response-renderer.d.ts.map +1 -0
  18. package/lib/browser/chat-response-renderer/delegation-response-renderer.js +144 -0
  19. package/lib/browser/chat-response-renderer/delegation-response-renderer.js.map +1 -0
  20. package/lib/browser/chat-response-renderer/index.d.ts +2 -0
  21. package/lib/browser/chat-response-renderer/index.d.ts.map +1 -1
  22. package/lib/browser/chat-response-renderer/index.js +2 -0
  23. package/lib/browser/chat-response-renderer/index.js.map +1 -1
  24. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts +17 -0
  25. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts.map +1 -0
  26. package/lib/browser/chat-response-renderer/tool-confirmation.js +120 -0
  27. package/lib/browser/chat-response-renderer/tool-confirmation.js.map +1 -0
  28. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts +5 -1
  29. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -1
  30. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +83 -19
  31. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
  32. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.d.ts +6 -1
  33. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.d.ts.map +1 -1
  34. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js +9 -0
  35. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js.map +1 -1
  36. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts +8 -0
  37. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts.map +1 -1
  38. package/lib/browser/chat-tree-view/chat-view-tree-widget.js +72 -3
  39. package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
  40. package/lib/browser/chat-tree-view/sub-chat-widget.d.ts +22 -0
  41. package/lib/browser/chat-tree-view/sub-chat-widget.d.ts.map +1 -0
  42. package/lib/browser/chat-tree-view/sub-chat-widget.js +92 -0
  43. package/lib/browser/chat-tree-view/sub-chat-widget.js.map +1 -0
  44. package/lib/browser/chat-view-commands.d.ts +1 -0
  45. package/lib/browser/chat-view-commands.d.ts.map +1 -1
  46. package/lib/browser/chat-view-commands.js +5 -0
  47. package/lib/browser/chat-view-commands.js.map +1 -1
  48. package/lib/browser/chat-view-contribution.js +2 -1
  49. package/lib/browser/chat-view-contribution.js.map +1 -1
  50. package/lib/browser/chat-view-widget.d.ts +6 -3
  51. package/lib/browser/chat-view-widget.d.ts.map +1 -1
  52. package/lib/browser/chat-view-widget.js +20 -10
  53. package/lib/browser/chat-view-widget.js.map +1 -1
  54. package/package.json +10 -10
  55. package/src/browser/ai-chat-ui-contribution.ts +166 -5
  56. package/src/browser/ai-chat-ui-frontend-module.ts +11 -0
  57. package/src/browser/chat-input-widget.tsx +280 -170
  58. package/src/browser/chat-node-toolbar-action-contribution.ts +14 -0
  59. package/src/browser/chat-response-renderer/delegation-response-renderer.tsx +177 -0
  60. package/src/browser/chat-response-renderer/index.ts +2 -0
  61. package/src/browser/chat-response-renderer/tool-confirmation.tsx +173 -0
  62. package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +115 -19
  63. package/src/browser/chat-tree-view/chat-view-tree-input-widget.tsx +16 -1
  64. package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +89 -5
  65. package/src/browser/chat-tree-view/sub-chat-widget.tsx +101 -0
  66. package/src/browser/chat-view-commands.ts +6 -0
  67. package/src/browser/chat-view-contribution.ts +1 -1
  68. package/src/browser/chat-view-widget.tsx +25 -12
  69. 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
+ }
@@ -23,3 +23,5 @@ export * from './text-part-renderer';
23
23
  export * from './toolcall-part-renderer';
24
24
  export * from './thinking-part-renderer';
25
25
  export * from './progress-part-renderer';
26
+ export * from './tool-confirmation';
27
+ export * from './delegation-response-renderer';
@@ -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
- return (
36
- <h4 className='theia-toolCall'>
37
- {response.finished ? (
38
- <details>
39
- <summary>{nls.localize('theia/ai/chat-ui/toolcall-part-renderer/finished', 'Ran')} {response.name}
40
- ({this.renderCollapsibleArguments(response.arguments)})
41
- </summary>
42
- <pre>{this.tryPrettyPrintJson(response)}</pre>
43
- </details>
44
- ) : (
45
- <span>
46
- <Spinner /> {nls.localizeByDefault('Running')} {response.name}({this.renderCollapsibleArguments(response.arguments)})
47
- </span>
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
- <i className="fa fa-spinner fa-spin"></i>
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
  }