@theia/ai-chat-ui 1.68.0-next.8 → 1.68.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.
Files changed (132) hide show
  1. package/README.md +33 -0
  2. package/lib/browser/ai-chat-ui-contribution.d.ts +2 -1
  3. package/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -1
  4. package/lib/browser/ai-chat-ui-contribution.js +41 -32
  5. package/lib/browser/ai-chat-ui-contribution.js.map +1 -1
  6. package/lib/browser/ai-chat-ui-frontend-module.d.ts.map +1 -1
  7. package/lib/browser/ai-chat-ui-frontend-module.js +15 -6
  8. package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -1
  9. package/lib/browser/change-set-actions/change-set-accept-action.d.ts +0 -1
  10. package/lib/browser/change-set-actions/change-set-accept-action.d.ts.map +1 -1
  11. package/lib/browser/change-set-actions/change-set-action-service.d.ts +0 -1
  12. package/lib/browser/change-set-actions/change-set-action-service.d.ts.map +1 -1
  13. package/lib/browser/change-set-actions/change-set-action-service.js +2 -2
  14. package/lib/browser/change-set-actions/change-set-action-service.js.map +1 -1
  15. package/lib/browser/chat-focus-contribution.d.ts +14 -0
  16. package/lib/browser/chat-focus-contribution.d.ts.map +1 -0
  17. package/lib/browser/chat-focus-contribution.js +102 -0
  18. package/lib/browser/chat-focus-contribution.js.map +1 -0
  19. package/lib/browser/chat-input-agent-suggestions.d.ts +0 -1
  20. package/lib/browser/chat-input-agent-suggestions.d.ts.map +1 -1
  21. package/lib/browser/chat-input-agent-suggestions.js +1 -1
  22. package/lib/browser/chat-input-agent-suggestions.js.map +1 -1
  23. package/lib/browser/chat-input-history-contribution.js +6 -10
  24. package/lib/browser/chat-input-history-contribution.js.map +1 -1
  25. package/lib/browser/chat-input-mode-contribution.js +1 -2
  26. package/lib/browser/chat-input-mode-contribution.js.map +1 -1
  27. package/lib/browser/chat-input-widget.d.ts +0 -1
  28. package/lib/browser/chat-input-widget.d.ts.map +1 -1
  29. package/lib/browser/chat-input-widget.js +106 -96
  30. package/lib/browser/chat-input-widget.js.map +1 -1
  31. package/lib/browser/chat-progress-message.d.ts +0 -1
  32. package/lib/browser/chat-progress-message.d.ts.map +1 -1
  33. package/lib/browser/chat-response-part-renderer.d.ts +0 -1
  34. package/lib/browser/chat-response-part-renderer.d.ts.map +1 -1
  35. package/lib/browser/chat-response-renderer/ai-selection-resolver.js +4 -5
  36. package/lib/browser/chat-response-renderer/ai-selection-resolver.js.map +1 -1
  37. package/lib/browser/chat-response-renderer/code-part-renderer.d.ts +0 -1
  38. package/lib/browser/chat-response-renderer/code-part-renderer.d.ts.map +1 -1
  39. package/lib/browser/chat-response-renderer/code-part-renderer.js +6 -9
  40. package/lib/browser/chat-response-renderer/code-part-renderer.js.map +1 -1
  41. package/lib/browser/chat-response-renderer/command-part-renderer.d.ts +0 -1
  42. package/lib/browser/chat-response-renderer/command-part-renderer.d.ts.map +1 -1
  43. package/lib/browser/chat-response-renderer/command-part-renderer.js +7 -4
  44. package/lib/browser/chat-response-renderer/command-part-renderer.js.map +1 -1
  45. package/lib/browser/chat-response-renderer/delegation-response-renderer.d.ts +0 -1
  46. package/lib/browser/chat-response-renderer/delegation-response-renderer.d.ts.map +1 -1
  47. package/lib/browser/chat-response-renderer/delegation-response-renderer.js +3 -4
  48. package/lib/browser/chat-response-renderer/delegation-response-renderer.js.map +1 -1
  49. package/lib/browser/chat-response-renderer/error-part-renderer.d.ts +0 -1
  50. package/lib/browser/chat-response-renderer/error-part-renderer.d.ts.map +1 -1
  51. package/lib/browser/chat-response-renderer/horizontal-layout-part-renderer.d.ts +0 -1
  52. package/lib/browser/chat-response-renderer/horizontal-layout-part-renderer.d.ts.map +1 -1
  53. package/lib/browser/chat-response-renderer/index.d.ts +1 -0
  54. package/lib/browser/chat-response-renderer/index.d.ts.map +1 -1
  55. package/lib/browser/chat-response-renderer/index.js +1 -0
  56. package/lib/browser/chat-response-renderer/index.js.map +1 -1
  57. package/lib/browser/chat-response-renderer/markdown-part-renderer.d.ts +0 -1
  58. package/lib/browser/chat-response-renderer/markdown-part-renderer.d.ts.map +1 -1
  59. package/lib/browser/chat-response-renderer/markdown-part-renderer.js +7 -9
  60. package/lib/browser/chat-response-renderer/markdown-part-renderer.js.map +1 -1
  61. package/lib/browser/chat-response-renderer/not-available-toolcall-renderer.d.ts +17 -0
  62. package/lib/browser/chat-response-renderer/not-available-toolcall-renderer.d.ts.map +1 -0
  63. package/lib/browser/chat-response-renderer/not-available-toolcall-renderer.js +56 -0
  64. package/lib/browser/chat-response-renderer/not-available-toolcall-renderer.js.map +1 -0
  65. package/lib/browser/chat-response-renderer/progress-part-renderer.d.ts +0 -1
  66. package/lib/browser/chat-response-renderer/progress-part-renderer.d.ts.map +1 -1
  67. package/lib/browser/chat-response-renderer/question-part-renderer.d.ts +0 -1
  68. package/lib/browser/chat-response-renderer/question-part-renderer.d.ts.map +1 -1
  69. package/lib/browser/chat-response-renderer/question-part-renderer.js +6 -9
  70. package/lib/browser/chat-response-renderer/question-part-renderer.js.map +1 -1
  71. package/lib/browser/chat-response-renderer/text-part-renderer.d.ts +0 -1
  72. package/lib/browser/chat-response-renderer/text-part-renderer.d.ts.map +1 -1
  73. package/lib/browser/chat-response-renderer/thinking-part-renderer.d.ts +0 -1
  74. package/lib/browser/chat-response-renderer/thinking-part-renderer.d.ts.map +1 -1
  75. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts +29 -10
  76. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts.map +1 -1
  77. package/lib/browser/chat-response-renderer/tool-confirmation.js +219 -65
  78. package/lib/browser/chat-response-renderer/tool-confirmation.js.map +1 -1
  79. package/lib/browser/chat-response-renderer/tool-confirmation.spec.d.ts +2 -0
  80. package/lib/browser/chat-response-renderer/tool-confirmation.spec.d.ts.map +1 -0
  81. package/lib/browser/chat-response-renderer/tool-confirmation.spec.js +100 -0
  82. package/lib/browser/chat-response-renderer/tool-confirmation.spec.js.map +1 -0
  83. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts +5 -3
  84. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -1
  85. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +21 -11
  86. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
  87. package/lib/browser/chat-response-renderer/unknown-part-renderer.d.ts +0 -1
  88. package/lib/browser/chat-response-renderer/unknown-part-renderer.d.ts.map +1 -1
  89. package/lib/browser/chat-response-renderer/unknown-part-renderer.js +1 -2
  90. package/lib/browser/chat-response-renderer/unknown-part-renderer.js.map +1 -1
  91. package/lib/browser/chat-tree-view/chat-view-tree-container.js +1 -2
  92. package/lib/browser/chat-tree-view/chat-view-tree-container.js.map +1 -1
  93. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js +2 -3
  94. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js.map +1 -1
  95. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts +3 -1
  96. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts.map +1 -1
  97. package/lib/browser/chat-tree-view/chat-view-tree-widget.js +49 -42
  98. package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
  99. package/lib/browser/chat-tree-view/prompt-variant-badge.d.ts +0 -1
  100. package/lib/browser/chat-tree-view/prompt-variant-badge.d.ts.map +1 -1
  101. package/lib/browser/chat-tree-view/sub-chat-widget.d.ts +0 -1
  102. package/lib/browser/chat-tree-view/sub-chat-widget.d.ts.map +1 -1
  103. package/lib/browser/chat-view-commands.d.ts.map +1 -1
  104. package/lib/browser/chat-view-commands.js +2 -2
  105. package/lib/browser/chat-view-commands.js.map +1 -1
  106. package/lib/browser/chat-view-contribution.js +7 -7
  107. package/lib/browser/chat-view-contribution.js.map +1 -1
  108. package/lib/browser/chat-view-language-contribution.js +3 -4
  109. package/lib/browser/chat-view-language-contribution.js.map +1 -1
  110. package/lib/browser/chat-view-widget.js +9 -10
  111. package/lib/browser/chat-view-widget.js.map +1 -1
  112. package/lib/browser/context-variable-picker.js +9 -13
  113. package/lib/browser/context-variable-picker.js.map +1 -1
  114. package/lib/browser/session-storage-preference-renderer.d.ts +127 -0
  115. package/lib/browser/session-storage-preference-renderer.d.ts.map +1 -0
  116. package/lib/browser/session-storage-preference-renderer.js +490 -0
  117. package/lib/browser/session-storage-preference-renderer.js.map +1 -0
  118. package/package.json +12 -11
  119. package/src/browser/ai-chat-ui-contribution.ts +19 -7
  120. package/src/browser/ai-chat-ui-frontend-module.ts +13 -0
  121. package/src/browser/chat-focus-contribution.ts +102 -0
  122. package/src/browser/chat-input-widget.tsx +99 -27
  123. package/src/browser/chat-response-renderer/index.ts +1 -0
  124. package/src/browser/chat-response-renderer/not-available-toolcall-renderer.tsx +57 -0
  125. package/src/browser/chat-response-renderer/tool-confirmation.spec.ts +108 -0
  126. package/src/browser/chat-response-renderer/tool-confirmation.tsx +379 -95
  127. package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +27 -13
  128. package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +33 -5
  129. package/src/browser/chat-view-commands.ts +2 -2
  130. package/src/browser/session-storage-preference-renderer.ts +657 -0
  131. package/src/browser/style/index.css +73 -26
  132. package/src/browser/style/session-storage-preference.css +134 -0
@@ -16,114 +16,214 @@
16
16
 
17
17
  import * as React from '@theia/core/shared/react';
18
18
  import { nls } from '@theia/core/lib/common/nls';
19
- import { codicon } from '@theia/core/lib/browser';
19
+ import { codicon, ContextMenuRenderer } from '@theia/core/lib/browser';
20
20
  import { ToolCallChatResponseContent } from '@theia/ai-chat/lib/common';
21
+ import { ToolRequest } from '@theia/ai-core';
22
+ import { CommandMenu, ContextExpressionMatcher, MenuPath } from '@theia/core/lib/common/menu';
23
+ import { GroupImpl } from '@theia/core/lib/browser/menu/composite-menu-node';
24
+ import { ToolConfirmationMode as ToolConfirmationPreferenceMode } from '@theia/ai-chat/lib/common/chat-tool-preferences';
25
+ import { ToolConfirmationManager } from '@theia/ai-chat/lib/browser/chat-tool-preference-bindings';
21
26
 
22
- /**
23
- * States the tool confirmation component can be in
24
- */
25
27
  export type ToolConfirmationState = 'waiting' | 'allowed' | 'denied' | 'rejected';
26
28
 
27
- export interface ToolConfirmationProps {
28
- response: ToolCallChatResponseContent;
29
- onAllow: (mode?: 'once' | 'session' | 'forever') => void;
30
- onDeny: (mode?: 'once' | 'session' | 'forever') => void;
29
+ export type ConfirmationScope = 'once' | 'session' | 'forever';
30
+
31
+ export interface ToolConfirmationCallbacks {
32
+ toolRequest?: ToolRequest;
33
+ onAllow: (scope: ConfirmationScope) => void;
34
+ onDeny: (scope: ConfirmationScope, reason?: string) => void;
31
35
  }
32
36
 
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);
37
+ export interface ToolConfirmationActionsProps extends ToolConfirmationCallbacks {
38
+ toolName: string;
39
+ contextMenuRenderer: ContextMenuRenderer;
40
+ }
42
41
 
43
- const handleAllow = React.useCallback(() => {
44
- setState('allowed');
45
- onAllow(allowMode);
46
- }, [onAllow, allowMode]);
42
+ class InlineActionMenuNode implements CommandMenu {
43
+ constructor(
44
+ readonly id: string,
45
+ readonly label: string,
46
+ private readonly action: () => void,
47
+ readonly sortString: string,
48
+ readonly icon?: string
49
+ ) { }
47
50
 
48
- const handleDeny = React.useCallback(() => {
49
- setState('denied');
50
- onDeny(denyMode);
51
- }, [onDeny, denyMode]);
51
+ isVisible<T>(_effectiveMenuPath: MenuPath, _contextMatcher: ContextExpressionMatcher<T>, _context: T | undefined): boolean {
52
+ return true;
53
+ }
52
54
 
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
- );
55
+ isEnabled(): boolean {
56
+ return true;
59
57
  }
60
58
 
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
- );
59
+ isToggled(): boolean {
60
+ return false;
61
+ }
62
+
63
+ async run(): Promise<void> {
64
+ this.action();
67
65
  }
66
+ }
67
+
68
+ export const ToolConfirmationActions: React.FC<ToolConfirmationActionsProps> = ({
69
+ toolName,
70
+ toolRequest,
71
+ onAllow,
72
+ onDeny,
73
+ contextMenuRenderer
74
+ }) => {
75
+ const [allowScope, setAllowScope] = React.useState<ConfirmationScope>('once');
76
+ const [denyScope, setDenyScope] = React.useState<ConfirmationScope>('once');
77
+ const [showAlwaysAllowConfirmation, setShowAlwaysAllowConfirmation] = React.useState(false);
78
+ const [showDenyReasonInput, setShowDenyReasonInput] = React.useState(false);
79
+ const [denyReason, setDenyReason] = React.useState('');
80
+ // eslint-disable-next-line no-null/no-null
81
+ const denyReasonInputRef = React.useRef<HTMLInputElement>(null);
82
+
83
+ const handleAllow = React.useCallback(() => {
84
+ if ((allowScope === 'forever' || allowScope === 'session') && toolRequest?.confirmAlwaysAllow) {
85
+ setShowAlwaysAllowConfirmation(true);
86
+ return;
87
+ }
88
+ onAllow(allowScope);
89
+ }, [onAllow, allowScope, toolRequest]);
90
+
91
+ const handleConfirmAlwaysAllow = React.useCallback(() => {
92
+ setShowAlwaysAllowConfirmation(false);
93
+ onAllow(allowScope);
94
+ }, [onAllow, allowScope]);
95
+
96
+ const handleCancelAlwaysAllow = React.useCallback(() => {
97
+ setShowAlwaysAllowConfirmation(false);
98
+ }, []);
99
+
100
+ const handleDeny = React.useCallback(() => {
101
+ onDeny(denyScope);
102
+ }, [onDeny, denyScope]);
103
+
104
+ const handleDenyWithReason = React.useCallback(() => {
105
+ setShowDenyReasonInput(true);
106
+ }, []);
107
+
108
+ const handleSubmitDenyReason = React.useCallback(() => {
109
+ onDeny('once', denyReason.trim() || undefined);
110
+ setShowDenyReasonInput(false);
111
+ setDenyReason('');
112
+ }, [onDeny, denyReason]);
113
+
114
+ const handleCancelDenyReason = React.useCallback(() => {
115
+ setShowDenyReasonInput(false);
116
+ setDenyReason('');
117
+ }, []);
118
+
119
+ const handleDenyReasonKeyDown = React.useCallback((e: React.KeyboardEvent) => {
120
+ if (e.key === 'Enter') {
121
+ e.preventDefault();
122
+ handleSubmitDenyReason();
123
+ } else if (e.key === 'Escape') {
124
+ e.preventDefault();
125
+ handleCancelDenyReason();
126
+ }
127
+ }, [handleSubmitDenyReason, handleCancelDenyReason]);
128
+
129
+ React.useEffect(() => {
130
+ if (showDenyReasonInput && denyReasonInputRef.current) {
131
+ denyReasonInputRef.current.focus();
132
+ }
133
+ }, [showDenyReasonInput]);
68
134
 
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') => {
135
+ const SCOPES: ConfirmationScope[] = ['once', 'session', 'forever'];
136
+
137
+ const scopeLabel = (type: 'allow' | 'deny', scope: ConfirmationScope): string => {
73
138
  if (type === 'allow') {
74
- switch (mode) {
139
+ switch (scope) {
75
140
  case 'once': return nls.localizeByDefault('Allow');
76
141
  case 'session': return nls.localize('theia/ai/chat-ui/toolconfirmation/allow-session', 'Allow for this Chat');
77
142
  case 'forever': return nls.localizeByDefault('Always Allow');
78
143
  }
79
144
  } else {
80
- switch (mode) {
145
+ switch (scope) {
81
146
  case 'once': return nls.localizeByDefault('Deny');
82
147
  case 'session': return nls.localize('theia/ai/chat-ui/toolconfirmation/deny-session', 'Deny for this Chat');
83
148
  case 'forever': return nls.localize('theia/ai/chat-ui/toolconfirmation/deny-forever', 'Always Deny');
84
149
  }
85
150
  }
86
151
  };
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
152
 
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
- }
153
+ const getAlwaysAllowWarning = (): string => {
154
+ if (typeof toolRequest?.confirmAlwaysAllow === 'string') {
155
+ return toolRequest.confirmAlwaysAllow;
104
156
  }
157
+ return nls.localize(
158
+ 'theia/ai/chat-ui/toolconfirmation/alwaysAllowGenericWarning',
159
+ 'This tool requires confirmation before auto-approval can be enabled. ' +
160
+ 'Once enabled, all future invocations will execute without confirmation. ' +
161
+ 'Only enable this if you trust this tool and understand the potential risks.'
162
+ );
105
163
  };
106
164
 
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;
165
+ const showDropdownMenu = React.useCallback((
166
+ event: React.MouseEvent<HTMLButtonElement>,
167
+ type: 'allow' | 'deny',
168
+ selectedScope: ConfirmationScope,
169
+ setScope: (scope: ConfirmationScope) => void
170
+ ) => {
171
+ const otherScopes = SCOPES.filter(s => s !== selectedScope);
172
+ const menu = new GroupImpl('tool-confirmation-dropdown');
173
+
174
+ const scopesGroup = new GroupImpl('scopes', '1');
175
+ otherScopes.forEach((scope, index) => {
176
+ scopesGroup.addNode(new InlineActionMenuNode(
177
+ `tool-confirmation-${type}-${scope}`,
178
+ scopeLabel(type, scope),
179
+ () => setScope(scope),
180
+ String(index)
181
+ ));
182
+ });
183
+ menu.addNode(scopesGroup);
184
+
185
+ if (type === 'deny') {
186
+ const reasonGroup = new GroupImpl('reason', '2');
187
+ reasonGroup.addNode(new InlineActionMenuNode(
188
+ 'tool-confirmation-deny-with-reason',
189
+ nls.localize('theia/ai/chat-ui/toolconfirmation/deny-with-reason', 'Deny with reason...'),
190
+ handleDenyWithReason,
191
+ '0'
192
+ ));
193
+ menu.addNode(reasonGroup);
194
+ }
195
+
196
+ const splitButtonContainer = event.currentTarget.parentElement;
197
+ const containerRect = splitButtonContainer?.getBoundingClientRect() ?? event.currentTarget.getBoundingClientRect();
198
+ contextMenuRenderer.render({
199
+ menuPath: ['tool-confirmation-context-menu'],
200
+ menu,
201
+ anchor: { x: containerRect.left, y: containerRect.bottom },
202
+ context: event.currentTarget,
203
+ skipSingleRootNode: true
204
+ });
205
+ }, [contextMenuRenderer, handleDenyWithReason, scopeLabel]);
206
+
207
+ const renderSplitButton = (type: 'allow' | 'deny'): React.ReactNode => {
208
+ const selectedScope = type === 'allow' ? allowScope : denyScope;
209
+ const setScope = type === 'allow' ? setAllowScope : setDenyScope;
111
210
  const handleMain = type === 'allow' ? handleAllow : handleDeny;
112
- const otherModes = MODES.filter(m => m !== selectedMode);
211
+
113
212
  return (
114
- <div className={`theia-tool-confirmation-split-button ${type}`}
115
- style={{ display: 'inline-flex', position: 'relative' }}>
213
+ <div
214
+ className={`theia-tool-confirmation-split-button ${type}`}
215
+ style={{ display: 'inline-flex', position: 'relative' }}
216
+ >
116
217
  <button
117
- className={`theia-button ${type === 'allow' ? 'primary' : 'secondary'} theia-tool-confirmation-main-btn`}
218
+ className={`theia-button ${type === 'allow' ? 'main' : 'secondary'} theia-tool-confirmation-main-btn`}
118
219
  onClick={handleMain}
119
220
  >
120
- {mainButtonLabel(type, selectedMode)}
221
+ {scopeLabel(type, selectedScope)}
121
222
  </button>
122
223
  <button
123
- className={`theia-button ${type === 'allow' ? 'primary' : 'secondary'} theia-tool-confirmation-chevron-btn`}
124
- onClick={() => setDropdownOpen(dropdownOpen === type ? undefined : type)}
224
+ className={`theia-button ${type === 'allow' ? 'main' : 'secondary'} theia-tool-confirmation-chevron-btn`}
225
+ onClick={e => showDropdownMenu(e, type, selectedScope, setScope)}
125
226
  aria-haspopup="true"
126
- aria-expanded={dropdownOpen === type}
127
227
  tabIndex={0}
128
228
  title={type === 'allow'
129
229
  ? nls.localize('theia/ai/chat-ui/toolconfirmation/allow-options-dropdown-tooltip', 'More Allow Options')
@@ -131,30 +231,112 @@ export const ToolConfirmation: React.FC<ToolConfirmationProps> = ({ response, on
131
231
  >
132
232
  <span className={codicon('chevron-down')}></span>
133
233
  </button>
134
- {dropdownOpen === type && (
135
- <ul
136
- className="theia-tool-confirmation-dropdown-menu"
137
- onMouseLeave={() => setDropdownOpen(undefined)}
138
- >
139
- {otherModes.map(mode => (
140
- <li
141
- key={mode}
142
- className="theia-tool-confirmation-dropdown-item"
143
- onClick={() => {
144
- setMode(mode);
145
- setDropdownOpen(undefined);
146
- }}
147
- title={modeTooltip(type, mode)}
148
- >
149
- {modeLabel(type, mode)}
150
- </li>
151
- ))}
152
- </ul>
153
- )}
154
234
  </div>
155
235
  );
156
236
  };
157
237
 
238
+ if (showAlwaysAllowConfirmation) {
239
+ return (
240
+ <div className="theia-tool-confirmation-always-allow-modal">
241
+ <div className="theia-tool-confirmation-header">
242
+ <span className={codicon('warning')}></span>
243
+ {nls.localize('theia/ai/chat-ui/toolconfirmation/alwaysAllowTitle', 'Enable Auto-Approval for "{0}"?', toolName)}
244
+ </div>
245
+ <div className="theia-tool-confirmation-warning">
246
+ {getAlwaysAllowWarning()}
247
+ </div>
248
+ <div className="theia-tool-confirmation-actions">
249
+ <button
250
+ className="theia-button secondary"
251
+ onClick={handleCancelAlwaysAllow}
252
+ >
253
+ {nls.localizeByDefault('Cancel')}
254
+ </button>
255
+ <button
256
+ className="theia-button main"
257
+ onClick={handleConfirmAlwaysAllow}
258
+ >
259
+ {nls.localize('theia/ai/chat-ui/toolconfirmation/alwaysAllowConfirm', 'I understand, enable auto-approval')}
260
+ </button>
261
+ </div>
262
+ </div>
263
+ );
264
+ }
265
+
266
+ if (showDenyReasonInput) {
267
+ return (
268
+ <div className="theia-tool-confirmation-deny-reason">
269
+ <input
270
+ ref={denyReasonInputRef}
271
+ type="text"
272
+ className="theia-input theia-tool-confirmation-deny-reason-input"
273
+ placeholder={nls.localize('theia/ai/chat-ui/toolconfirmation/deny-reason-placeholder', 'Enter reason for denial...')}
274
+ value={denyReason}
275
+ onChange={e => setDenyReason(e.target.value)}
276
+ onKeyDown={handleDenyReasonKeyDown}
277
+ />
278
+ <div className="theia-tool-confirmation-deny-reason-actions">
279
+ <button
280
+ className="theia-button secondary"
281
+ onClick={handleCancelDenyReason}
282
+ >
283
+ {nls.localizeByDefault('Cancel')}
284
+ </button>
285
+ <button
286
+ className="theia-button main"
287
+ onClick={handleSubmitDenyReason}
288
+ >
289
+ {nls.localizeByDefault('Deny')}
290
+ </button>
291
+ </div>
292
+ </div>
293
+ );
294
+ }
295
+
296
+ return (
297
+ <div className="theia-tool-confirmation-actions">
298
+ {renderSplitButton('deny')}
299
+ {renderSplitButton('allow')}
300
+ </div>
301
+ );
302
+ };
303
+
304
+ export interface ToolConfirmationProps extends Pick<ToolConfirmationCallbacks, 'toolRequest'> {
305
+ response: ToolCallChatResponseContent;
306
+ onAllow: (scope?: ConfirmationScope) => void;
307
+ onDeny: (scope?: ConfirmationScope, reason?: string) => void;
308
+ contextMenuRenderer: ContextMenuRenderer;
309
+ }
310
+
311
+ export const ToolConfirmation: React.FC<ToolConfirmationProps> = ({ response, toolRequest, onAllow, onDeny, contextMenuRenderer }) => {
312
+ const [state, setState] = React.useState<ToolConfirmationState>('waiting');
313
+
314
+ const handleAllow = React.useCallback((scope: ConfirmationScope) => {
315
+ setState('allowed');
316
+ onAllow(scope);
317
+ }, [onAllow]);
318
+
319
+ const handleDeny = React.useCallback((scope: ConfirmationScope, reason?: string) => {
320
+ setState('denied');
321
+ onDeny(scope, reason);
322
+ }, [onDeny]);
323
+
324
+ if (state === 'allowed') {
325
+ return (
326
+ <div className="theia-tool-confirmation-status allowed">
327
+ <span className={codicon('check')}></span> {nls.localize('theia/ai/chat-ui/toolconfirmation/allowed', 'Tool execution allowed')}
328
+ </div>
329
+ );
330
+ }
331
+
332
+ if (state === 'denied') {
333
+ return (
334
+ <div className="theia-tool-confirmation-status denied">
335
+ <span className={codicon('close')}></span> {nls.localize('theia/ai/chat-ui/toolconfirmation/denied', 'Tool execution denied')}
336
+ </div>
337
+ );
338
+ }
339
+
158
340
  return (
159
341
  <div className="theia-tool-confirmation">
160
342
  <div className="theia-tool-confirmation-header">
@@ -166,10 +348,112 @@ export const ToolConfirmation: React.FC<ToolConfirmationProps> = ({ response, on
166
348
  <span className="value">{response.name}</span>
167
349
  </div>
168
350
  </div>
169
- <div className="theia-tool-confirmation-actions">
170
- {renderSplitButton('deny')}
171
- {renderSplitButton('allow')}
172
- </div>
351
+ <ToolConfirmationActions
352
+ toolName={response.name ?? 'unknown'}
353
+ toolRequest={toolRequest}
354
+ onAllow={handleAllow}
355
+ onDeny={handleDeny}
356
+ contextMenuRenderer={contextMenuRenderer}
357
+ />
173
358
  </div>
174
359
  );
175
360
  };
361
+
362
+ export interface WithToolCallConfirmationProps {
363
+ response: ToolCallChatResponseContent;
364
+ confirmationMode: ToolConfirmationPreferenceMode;
365
+ toolConfirmationManager: ToolConfirmationManager;
366
+ toolRequest?: ToolRequest;
367
+ chatId: string;
368
+ requestCanceled: boolean;
369
+ contextMenuRenderer: ContextMenuRenderer;
370
+ }
371
+
372
+ export function withToolCallConfirmation<P extends object>(
373
+ WrappedComponent: React.ComponentType<P>
374
+ ): React.FC<P & WithToolCallConfirmationProps> {
375
+ const WithConfirmation: React.FC<P & WithToolCallConfirmationProps> = props => {
376
+ const {
377
+ response,
378
+ confirmationMode,
379
+ toolConfirmationManager,
380
+ toolRequest,
381
+ chatId,
382
+ requestCanceled,
383
+ contextMenuRenderer,
384
+ ...componentProps
385
+ } = props;
386
+
387
+ const [confirmationState, setConfirmationState] = React.useState<ToolConfirmationState>('waiting');
388
+
389
+ React.useEffect(() => {
390
+ if (confirmationMode === ToolConfirmationPreferenceMode.ALWAYS_ALLOW) {
391
+ response.confirm();
392
+ setConfirmationState('allowed');
393
+ return;
394
+ } else if (confirmationMode === ToolConfirmationPreferenceMode.DISABLED) {
395
+ response.deny();
396
+ setConfirmationState('denied');
397
+ return;
398
+ }
399
+ response.confirmed
400
+ .then(confirmed => {
401
+ setConfirmationState(confirmed === true ? 'allowed' : 'denied');
402
+ })
403
+ .catch(() => {
404
+ setConfirmationState('rejected');
405
+ });
406
+ }, [response, confirmationMode]);
407
+
408
+ const handleAllow = React.useCallback((scope: ConfirmationScope = 'once') => {
409
+ if (scope === 'forever' && response.name) {
410
+ toolConfirmationManager.setConfirmationMode(response.name, ToolConfirmationPreferenceMode.ALWAYS_ALLOW, toolRequest);
411
+ } else if (scope === 'session' && response.name) {
412
+ toolConfirmationManager.setSessionConfirmationMode(response.name, ToolConfirmationPreferenceMode.ALWAYS_ALLOW, chatId);
413
+ }
414
+ response.confirm();
415
+ }, [response, toolConfirmationManager, chatId, toolRequest]);
416
+
417
+ const handleDeny = React.useCallback((scope: ConfirmationScope = 'once', reason?: string) => {
418
+ if (scope === 'forever' && response.name) {
419
+ toolConfirmationManager.setConfirmationMode(response.name, ToolConfirmationPreferenceMode.DISABLED);
420
+ } else if (scope === 'session' && response.name) {
421
+ toolConfirmationManager.setSessionConfirmationMode(response.name, ToolConfirmationPreferenceMode.DISABLED, chatId);
422
+ }
423
+ response.deny(reason);
424
+ }, [response, toolConfirmationManager, chatId]);
425
+
426
+ if (confirmationState === 'rejected' || (requestCanceled && !response.finished)) {
427
+ return (
428
+ <div className="theia-tool-confirmation-status rejected">
429
+ <span className={codicon('error')}></span> {nls.localize('theia/ai/chat-ui/toolconfirmation/canceled', 'Tool execution canceled')}
430
+ </div>
431
+ );
432
+ }
433
+
434
+ if (confirmationState === 'denied') {
435
+ return (
436
+ <div className="theia-tool-confirmation-status denied">
437
+ <span className={codicon('error')}></span> {nls.localize('theia/ai/chat-ui/toolconfirmation/executionDenied', 'Tool execution denied')}
438
+ </div>
439
+ );
440
+ }
441
+
442
+ if (confirmationState === 'waiting' && !requestCanceled && !response.finished) {
443
+ return (
444
+ <ToolConfirmation
445
+ response={response}
446
+ toolRequest={toolRequest}
447
+ onAllow={handleAllow}
448
+ onDeny={handleDeny}
449
+ contextMenuRenderer={contextMenuRenderer}
450
+ />
451
+ );
452
+ }
453
+
454
+ return <WrappedComponent {...componentProps as P} />;
455
+ };
456
+
457
+ WithConfirmation.displayName = `withToolCallConfirmation(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`;
458
+ return WithConfirmation;
459
+ }
@@ -19,13 +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, OpenerService } from '@theia/core/lib/browser';
22
+ import { codicon, ContextMenuRenderer, 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 { ToolConfirmationMode } from '@theia/ai-chat/lib/common/chat-tool-preferences';
26
26
  import { ResponseNode } from '../chat-tree-view';
27
27
  import { useMarkdownRendering } from './markdown-part-renderer';
28
- import { ToolCallResult } from '@theia/ai-core';
28
+ import { ToolCallResult, ToolInvocationRegistry, ToolRequest } from '@theia/ai-core';
29
29
  import { ToolConfirmationManager } from '@theia/ai-chat/lib/browser/chat-tool-preference-bindings';
30
30
 
31
31
  @injectable()
@@ -37,6 +37,12 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
37
37
  @inject(OpenerService)
38
38
  protected openerService: OpenerService;
39
39
 
40
+ @inject(ToolInvocationRegistry)
41
+ protected toolInvocationRegistry: ToolInvocationRegistry;
42
+
43
+ @inject(ContextMenuRenderer)
44
+ protected contextMenuRenderer: ContextMenuRenderer;
45
+
40
46
  canHandle(response: ChatResponseContent): number {
41
47
  if (ToolCallChatResponseContent.is(response)) {
42
48
  return 10;
@@ -46,15 +52,18 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
46
52
 
47
53
  render(response: ToolCallChatResponseContent, parentNode: ResponseNode): ReactNode {
48
54
  const chatId = parentNode.sessionId;
49
- const confirmationMode = response.name ? this.getToolConfirmationSettings(response.name, chatId) : ToolConfirmationMode.DISABLED;
55
+ const toolRequest = response.name ? this.toolInvocationRegistry.getFunction(response.name) : undefined;
56
+ const confirmationMode = response.name ? this.getToolConfirmationSettings(response.name, chatId, toolRequest) : ToolConfirmationMode.DISABLED;
50
57
  return <ToolCallContent
51
58
  response={response}
52
59
  confirmationMode={confirmationMode}
53
60
  toolConfirmationManager={this.toolConfirmationManager}
61
+ toolRequest={toolRequest}
54
62
  chatId={chatId}
55
63
  renderCollapsibleArguments={this.renderCollapsibleArguments.bind(this)}
56
64
  responseRenderer={this.renderResult.bind(this)}
57
- requestCanceled={parentNode.response.isCanceled} />;
65
+ requestCanceled={parentNode.response.isCanceled}
66
+ contextMenuRenderer={this.contextMenuRenderer} />;
58
67
  }
59
68
 
60
69
  protected renderResult(response: ToolCallChatResponseContent): ReactNode {
@@ -102,8 +111,8 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
102
111
  }
103
112
  }
104
113
 
105
- protected getToolConfirmationSettings(responseId: string, chatId: string): ToolConfirmationMode {
106
- return this.toolConfirmationManager.getConfirmationMode(responseId, chatId);
114
+ protected getToolConfirmationSettings(responseId: string, chatId: string, toolRequest?: ToolRequest): ToolConfirmationMode {
115
+ return this.toolConfirmationManager.getConfirmationMode(responseId, chatId, toolRequest);
107
116
  }
108
117
 
109
118
  protected renderCollapsibleArguments(args: string | undefined): ReactNode {
@@ -137,10 +146,12 @@ interface ToolCallContentProps {
137
146
  response: ToolCallChatResponseContent;
138
147
  confirmationMode: ToolConfirmationMode;
139
148
  toolConfirmationManager: ToolConfirmationManager;
149
+ toolRequest?: ToolRequest;
140
150
  chatId: string;
141
151
  renderCollapsibleArguments: (args: string | undefined) => ReactNode;
142
152
  responseRenderer: (response: ToolCallChatResponseContent) => ReactNode | undefined;
143
153
  requestCanceled: boolean;
154
+ contextMenuRenderer: ContextMenuRenderer;
144
155
  }
145
156
 
146
157
  /**
@@ -150,10 +161,12 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({
150
161
  response,
151
162
  confirmationMode,
152
163
  toolConfirmationManager,
164
+ toolRequest,
153
165
  chatId,
154
166
  responseRenderer,
155
167
  renderCollapsibleArguments,
156
- requestCanceled
168
+ requestCanceled,
169
+ contextMenuRenderer
157
170
  }) => {
158
171
  const [confirmationState, setConfirmationState] = React.useState<ToolConfirmationState>('waiting');
159
172
  const [rejectionReason, setRejectionReason] = React.useState<unknown>(undefined);
@@ -201,20 +214,20 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({
201
214
 
202
215
  const handleAllow = React.useCallback((mode: 'once' | 'session' | 'forever' = 'once') => {
203
216
  if (mode === 'forever' && response.name) {
204
- toolConfirmationManager.setConfirmationMode(response.name, ToolConfirmationMode.ALWAYS_ALLOW);
217
+ toolConfirmationManager.setConfirmationMode(response.name, ToolConfirmationMode.ALWAYS_ALLOW, toolRequest);
205
218
  } else if (mode === 'session' && response.name) {
206
219
  toolConfirmationManager.setSessionConfirmationMode(response.name, ToolConfirmationMode.ALWAYS_ALLOW, chatId);
207
220
  }
208
221
  response.confirm();
209
- }, [response, toolConfirmationManager, chatId]);
222
+ }, [response, toolConfirmationManager, chatId, toolRequest]);
210
223
 
211
- const handleDeny = React.useCallback((mode: 'once' | 'session' | 'forever' = 'once') => {
224
+ const handleDeny = React.useCallback((mode: 'once' | 'session' | 'forever' = 'once', reason?: string) => {
212
225
  if (mode === 'forever' && response.name) {
213
226
  toolConfirmationManager.setConfirmationMode(response.name, ToolConfirmationMode.DISABLED);
214
227
  } else if (mode === 'session' && response.name) {
215
228
  toolConfirmationManager.setSessionConfirmationMode(response.name, ToolConfirmationMode.DISABLED, chatId);
216
229
  }
217
- response.deny();
230
+ response.deny(reason);
218
231
  }, [response, toolConfirmationManager, chatId]);
219
232
 
220
233
  const reasonText = formatReason(rejectionReason);
@@ -252,13 +265,14 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({
252
265
  )
253
266
  )}
254
267
 
255
- {/* Show confirmation UI when waiting for allow */}
256
- {confirmationState === 'waiting' && !requestCanceled && (
268
+ {confirmationState === 'waiting' && !requestCanceled && !response.finished && (
257
269
  <span className='theia-toolCall-waiting'>
258
270
  <ToolConfirmation
259
271
  response={response}
272
+ toolRequest={toolRequest}
260
273
  onAllow={handleAllow}
261
274
  onDeny={handleDeny}
275
+ contextMenuRenderer={contextMenuRenderer}
262
276
  />
263
277
  </span>
264
278
  )}