@theia/ai-chat-ui 1.71.0-next.8 → 1.71.1

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 (106) hide show
  1. package/lib/browser/ai-chat-ui-contribution.d.ts.map +1 -1
  2. package/lib/browser/ai-chat-ui-contribution.js +10 -9
  3. package/lib/browser/ai-chat-ui-contribution.js.map +1 -1
  4. package/lib/browser/ai-chat-ui-frontend-module.d.ts +1 -0
  5. package/lib/browser/ai-chat-ui-frontend-module.d.ts.map +1 -1
  6. package/lib/browser/ai-chat-ui-frontend-module.js +3 -0
  7. package/lib/browser/ai-chat-ui-frontend-module.js.map +1 -1
  8. package/lib/browser/chat-input-mode-contribution.js +1 -1
  9. package/lib/browser/chat-input-mode-contribution.js.map +1 -1
  10. package/lib/browser/chat-input-widget.d.ts +57 -3
  11. package/lib/browser/chat-input-widget.d.ts.map +1 -1
  12. package/lib/browser/chat-input-widget.js +382 -21
  13. package/lib/browser/chat-input-widget.js.map +1 -1
  14. package/lib/browser/chat-response-part-renderer.d.ts +6 -0
  15. package/lib/browser/chat-response-part-renderer.d.ts.map +1 -1
  16. package/lib/browser/chat-response-renderer/delegation-tool-renderer.d.ts +2 -0
  17. package/lib/browser/chat-response-renderer/delegation-tool-renderer.d.ts.map +1 -1
  18. package/lib/browser/chat-response-renderer/delegation-tool-renderer.js +76 -5
  19. package/lib/browser/chat-response-renderer/delegation-tool-renderer.js.map +1 -1
  20. package/lib/browser/chat-response-renderer/index.d.ts +1 -0
  21. package/lib/browser/chat-response-renderer/index.d.ts.map +1 -1
  22. package/lib/browser/chat-response-renderer/index.js +1 -0
  23. package/lib/browser/chat-response-renderer/index.js.map +1 -1
  24. package/lib/browser/chat-response-renderer/question-part-renderer.d.ts +3 -0
  25. package/lib/browser/chat-response-renderer/question-part-renderer.d.ts.map +1 -1
  26. package/lib/browser/chat-response-renderer/question-part-renderer.js +19 -6
  27. package/lib/browser/chat-response-renderer/question-part-renderer.js.map +1 -1
  28. package/lib/browser/chat-response-renderer/tool-call-rendering.d.ts +21 -0
  29. package/lib/browser/chat-response-renderer/tool-call-rendering.d.ts.map +1 -0
  30. package/lib/browser/chat-response-renderer/tool-call-rendering.js +53 -0
  31. package/lib/browser/chat-response-renderer/tool-call-rendering.js.map +1 -0
  32. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts +4 -0
  33. package/lib/browser/chat-response-renderer/tool-confirmation.d.ts.map +1 -1
  34. package/lib/browser/chat-response-renderer/tool-confirmation.js +22 -0
  35. package/lib/browser/chat-response-renderer/tool-confirmation.js.map +1 -1
  36. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts +1 -0
  37. package/lib/browser/chat-response-renderer/toolcall-part-renderer.d.ts.map +1 -1
  38. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js +7 -18
  39. package/lib/browser/chat-response-renderer/toolcall-part-renderer.js.map +1 -1
  40. package/lib/browser/chat-response-renderer/toolcall-utils.d.ts +1 -0
  41. package/lib/browser/chat-response-renderer/toolcall-utils.d.ts.map +1 -1
  42. package/lib/browser/chat-response-renderer/toolcall-utils.js +3 -0
  43. package/lib/browser/chat-response-renderer/toolcall-utils.js.map +1 -1
  44. package/lib/browser/chat-token-usage-indicator-util.d.ts +35 -0
  45. package/lib/browser/chat-token-usage-indicator-util.d.ts.map +1 -0
  46. package/lib/browser/chat-token-usage-indicator-util.js +138 -0
  47. package/lib/browser/chat-token-usage-indicator-util.js.map +1 -0
  48. package/lib/browser/chat-token-usage-indicator-util.spec.d.ts +2 -0
  49. package/lib/browser/chat-token-usage-indicator-util.spec.d.ts.map +1 -0
  50. package/lib/browser/chat-token-usage-indicator-util.spec.js +226 -0
  51. package/lib/browser/chat-token-usage-indicator-util.spec.js.map +1 -0
  52. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.d.ts +6 -1
  53. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.d.ts.map +1 -1
  54. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js +7 -0
  55. package/lib/browser/chat-tree-view/chat-view-tree-input-widget.js.map +1 -1
  56. package/lib/browser/chat-tree-view/chat-view-tree-widget.d.ts.map +1 -1
  57. package/lib/browser/chat-tree-view/chat-view-tree-widget.js +20 -3
  58. package/lib/browser/chat-tree-view/chat-view-tree-widget.js.map +1 -1
  59. package/lib/browser/chat-view-preferences.d.ts +18 -0
  60. package/lib/browser/chat-view-preferences.d.ts.map +1 -0
  61. package/lib/browser/chat-view-preferences.js +69 -0
  62. package/lib/browser/chat-view-preferences.js.map +1 -0
  63. package/lib/browser/chat-view-widget-toolbar-contribution.d.ts.map +1 -1
  64. package/lib/browser/chat-view-widget-toolbar-contribution.js +3 -2
  65. package/lib/browser/chat-view-widget-toolbar-contribution.js.map +1 -1
  66. package/lib/browser/chat-view-widget.d.ts +1 -2
  67. package/lib/browser/chat-view-widget.d.ts.map +1 -1
  68. package/lib/browser/chat-view-widget.js +3 -7
  69. package/lib/browser/chat-view-widget.js.map +1 -1
  70. package/lib/browser/session-settings-dialog.d.ts +2 -5
  71. package/lib/browser/session-settings-dialog.d.ts.map +1 -1
  72. package/lib/browser/session-settings-dialog.js +15 -33
  73. package/lib/browser/session-settings-dialog.js.map +1 -1
  74. package/lib/common/toolcall-utils.d.ts +6 -0
  75. package/lib/common/toolcall-utils.d.ts.map +1 -0
  76. package/lib/common/toolcall-utils.js +40 -0
  77. package/lib/common/toolcall-utils.js.map +1 -0
  78. package/lib/common/toolcall-utils.spec.d.ts +2 -0
  79. package/lib/common/toolcall-utils.spec.d.ts.map +1 -0
  80. package/lib/common/toolcall-utils.spec.js +102 -0
  81. package/lib/common/toolcall-utils.spec.js.map +1 -0
  82. package/package.json +12 -12
  83. package/src/browser/ai-chat-ui-contribution.ts +10 -9
  84. package/src/browser/ai-chat-ui-frontend-module.ts +4 -0
  85. package/src/browser/chat-input-mode-contribution.ts +1 -1
  86. package/src/browser/chat-input-widget.tsx +481 -14
  87. package/src/browser/chat-response-part-renderer.ts +6 -0
  88. package/src/browser/chat-response-renderer/delegation-tool-renderer.tsx +105 -7
  89. package/src/browser/chat-response-renderer/index.ts +1 -0
  90. package/src/browser/chat-response-renderer/question-part-renderer.tsx +24 -8
  91. package/src/browser/chat-response-renderer/tool-call-rendering.tsx +89 -0
  92. package/src/browser/chat-response-renderer/tool-confirmation.tsx +26 -0
  93. package/src/browser/chat-response-renderer/toolcall-part-renderer.tsx +21 -18
  94. package/src/browser/chat-response-renderer/toolcall-utils.ts +2 -0
  95. package/src/browser/chat-token-usage-indicator-util.spec.ts +262 -0
  96. package/src/browser/chat-token-usage-indicator-util.ts +151 -0
  97. package/src/browser/chat-tree-view/chat-view-tree-input-widget.tsx +9 -1
  98. package/src/browser/chat-tree-view/chat-view-tree-widget.tsx +22 -3
  99. package/src/browser/chat-view-preferences.ts +87 -0
  100. package/src/browser/chat-view-widget-toolbar-contribution.tsx +3 -2
  101. package/src/browser/chat-view-widget.tsx +4 -7
  102. package/src/browser/session-settings-dialog.tsx +15 -97
  103. package/src/browser/style/index.css +164 -54
  104. package/src/browser/style/tool-call-rendering.css +327 -0
  105. package/src/common/toolcall-utils.spec.ts +118 -0
  106. package/src/common/toolcall-utils.ts +36 -0
@@ -22,4 +22,10 @@ export const ChatResponsePartRenderer = Symbol('ChatResponsePartRenderer');
22
22
  export interface ChatResponsePartRenderer<T extends ChatResponseContent> {
23
23
  canHandle(response: ChatResponseContent): number;
24
24
  render(response: T, parentNode: ResponseNode): ReactNode;
25
+ /**
26
+ * Optional method to render a compact confirmation/interaction view for this content type.
27
+ * Used by the delegation summary to show tool-specific confirmation UI in collapsed state.
28
+ * If not implemented, a generic fallback confirmation UI is used.
29
+ */
30
+ renderConfirmation?(response: T, parentNode: ResponseNode): ReactNode;
25
31
  }
@@ -14,8 +14,8 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
 
17
- import { inject, injectable } from '@theia/core/shared/inversify';
18
- import { ChatRequestInvocation, ChatResponseContent, ChatResponseModel, ToolCallChatResponseContent } from '@theia/ai-chat';
17
+ import { inject, injectable, named } from '@theia/core/shared/inversify';
18
+ import { ChatRequestInvocation, ChatResponseContent, ChatResponseModel, InteractiveContent, ToolCallChatResponseContent } from '@theia/ai-chat';
19
19
  import { ChatAgentService } from '@theia/ai-chat/lib/common/chat-agent-service';
20
20
  import { ToolConfirmationManager } from '@theia/ai-chat/lib/browser/chat-tool-preference-bindings';
21
21
  import { AGENT_DELEGATION_FUNCTION_ID } from '@theia/ai-core/lib/common/tool-constants';
@@ -25,8 +25,9 @@ import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
25
25
  import { ResponseNode } from '../chat-tree-view';
26
26
  import { SubChatWidgetFactory } from '../chat-tree-view/sub-chat-widget';
27
27
  import { withToolCallConfirmation } from './tool-confirmation';
28
+ import { extractJsonStringField } from './toolcall-utils';
28
29
  import { CompositeTreeNode, ContextMenuRenderer } from '@theia/core/lib/browser';
29
- import { DisposableCollection, nls } from '@theia/core';
30
+ import { ContributionProvider, DisposableCollection, nls } from '@theia/core';
30
31
  import * as React from '@theia/core/shared/react';
31
32
 
32
33
  @injectable()
@@ -50,6 +51,9 @@ export class DelegationToolRenderer implements ChatResponsePartRenderer<ToolCall
50
51
  @inject(ContextMenuRenderer)
51
52
  protected contextMenuRenderer: ContextMenuRenderer;
52
53
 
54
+ @inject(ContributionProvider) @named(ChatResponsePartRenderer)
55
+ protected chatResponsePartRenderers: ContributionProvider<ChatResponsePartRenderer<ChatResponseContent>>;
56
+
53
57
  canHandle(response: ChatResponseContent): number {
54
58
  if (ToolCallChatResponseContent.is(response) && response.name === AGENT_DELEGATION_FUNCTION_ID) {
55
59
  return 20;
@@ -72,7 +76,14 @@ export class DelegationToolRenderer implements ChatResponsePartRenderer<ToolCall
72
76
  prompt = args.prompt;
73
77
  }
74
78
  } catch {
75
- // ignore parse errors
79
+ const partialAgentId = extractJsonStringField(response.arguments, 'agentId');
80
+ if (partialAgentId) {
81
+ agentName = this.chatAgentService.getAgent(partialAgentId)?.name ?? partialAgentId;
82
+ }
83
+ const partialPrompt = extractJsonStringField(response.arguments, 'prompt');
84
+ if (partialPrompt) {
85
+ prompt = partialPrompt;
86
+ }
76
87
  }
77
88
  }
78
89
 
@@ -87,17 +98,23 @@ export class DelegationToolRenderer implements ChatResponsePartRenderer<ToolCall
87
98
  finished={response.finished}
88
99
  parentNode={parentNode}
89
100
  subChatWidgetFactory={this.subChatWidgetFactory}
101
+ contextMenuRenderer={this.contextMenuRenderer}
102
+ chatResponsePartRenderers={this.chatResponsePartRenderers}
90
103
  response={response}
91
104
  confirmationMode={confirmationMode}
92
105
  toolConfirmationManager={this.toolConfirmationManager}
93
106
  toolRequest={toolRequest}
94
107
  chatId={chatId}
95
108
  requestCanceled={parentNode.response.isCanceled}
96
- contextMenuRenderer={this.contextMenuRenderer}
97
109
  />;
98
110
  }
99
111
  }
100
112
 
113
+ interface PendingInteraction {
114
+ contentPart: InteractiveContent & ChatResponseContent;
115
+ id: string;
116
+ }
117
+
101
118
  interface DelegatedChatProps {
102
119
  invocation?: ChatRequestInvocation;
103
120
  agentName: string;
@@ -105,19 +122,28 @@ interface DelegatedChatProps {
105
122
  finished?: boolean;
106
123
  parentNode: ResponseNode;
107
124
  subChatWidgetFactory: SubChatWidgetFactory;
125
+ contextMenuRenderer: ContextMenuRenderer;
126
+ chatResponsePartRenderers: ContributionProvider<ChatResponsePartRenderer<ChatResponseContent>>;
108
127
  }
109
128
 
110
129
  interface DelegatedChatState {
111
130
  node?: ResponseNode;
131
+ isOpen: boolean;
132
+ pendingInteractions: PendingInteraction[];
112
133
  }
113
134
 
114
135
  class DelegatedChat extends React.Component<DelegatedChatProps, DelegatedChatState> {
115
136
  private widget: ReturnType<SubChatWidgetFactory>;
116
137
  private toDispose = new DisposableCollection();
138
+ private trackedInteractionIds = new Set<string>();
117
139
 
118
140
  constructor(props: DelegatedChatProps) {
119
141
  super(props);
120
- this.state = { node: undefined };
142
+ this.state = {
143
+ node: undefined,
144
+ isOpen: false,
145
+ pendingInteractions: []
146
+ };
121
147
  this.widget = props.subChatWidgetFactory();
122
148
  }
123
149
 
@@ -134,6 +160,7 @@ class DelegatedChat extends React.Component<DelegatedChatProps, DelegatedChatSta
134
160
  private subscribeToInvocation(invocation?: ChatRequestInvocation): void {
135
161
  this.toDispose.dispose();
136
162
  this.toDispose = new DisposableCollection();
163
+ this.trackedInteractionIds.clear();
137
164
  if (!invocation) {
138
165
  return;
139
166
  }
@@ -143,9 +170,25 @@ class DelegatedChat extends React.Component<DelegatedChatProps, DelegatedChatSta
143
170
  this.setState({ node });
144
171
 
145
172
  const changeListener = () => {
173
+ this.removeResolvedInteractions();
146
174
  this.forceUpdate();
147
175
  };
148
176
  this.toDispose.push(chatModel.onDidChange(changeListener));
177
+
178
+ // Subscribe to interactionNeeded for push-based interaction tracking
179
+ this.toDispose.push(chatModel.onInteractionNeeded(contentPart => {
180
+ const id = contentPart.interactionId;
181
+ if (id && !this.trackedInteractionIds.has(id)
182
+ && this.findConfirmationRenderer(contentPart)) {
183
+ this.trackedInteractionIds.add(id);
184
+ this.setState(prevState => ({
185
+ pendingInteractions: [
186
+ ...prevState.pendingInteractions,
187
+ { contentPart, id }
188
+ ]
189
+ }));
190
+ }
191
+ }));
149
192
  }).catch(error => {
150
193
  console.error('Failed to create delegated chat response:', error);
151
194
  });
@@ -160,6 +203,42 @@ class DelegatedChat extends React.Component<DelegatedChatProps, DelegatedChatSta
160
203
 
161
204
  override componentWillUnmount(): void {
162
205
  this.toDispose.dispose();
206
+ this.trackedInteractionIds.clear();
207
+ }
208
+
209
+ private removeResolvedInteractions(): void {
210
+ this.setState(prevState => ({
211
+ pendingInteractions: prevState.pendingInteractions.filter(p => !p.contentPart.isResolved)
212
+ }));
213
+ }
214
+
215
+ private findConfirmationRenderer(contentPart: ChatResponseContent): ChatResponsePartRenderer<ChatResponseContent> | undefined {
216
+ const renderer = this.props.chatResponsePartRenderers.getContributions().reduce<[number, ChatResponsePartRenderer<ChatResponseContent> | undefined]>(
217
+ (prev, current) => {
218
+ const prio = current.canHandle(contentPart);
219
+ if (prio > prev[0]) {
220
+ return [prio, current];
221
+ }
222
+ return prev;
223
+ },
224
+ [-1, undefined])[1];
225
+ if (renderer && renderer.renderConfirmation) {
226
+ return renderer;
227
+ }
228
+ return undefined;
229
+ }
230
+
231
+ private handleToggle = (event: React.SyntheticEvent<HTMLDetailsElement>): void => {
232
+ const details = event.currentTarget;
233
+ this.setState({ isOpen: details.open });
234
+ };
235
+
236
+ private renderInteractionConfirmation(contentPart: ChatResponseContent, id: string): React.ReactNode {
237
+ const renderer = this.findConfirmationRenderer(contentPart);
238
+ if (renderer && this.state.node) {
239
+ return <React.Fragment key={id}>{renderer.renderConfirmation!(contentPart, this.state.node)}</React.Fragment>;
240
+ }
241
+ return undefined;
163
242
  }
164
243
 
165
244
  override render(): React.ReactNode {
@@ -193,19 +272,38 @@ class DelegatedChat extends React.Component<DelegatedChatProps, DelegatedChatSta
193
272
  statusText = nls.localize('theia/ai/chat-ui/delegation-response-renderer/status/starting', 'starting...');
194
273
  }
195
274
 
275
+ const { isOpen, pendingInteractions } = this.state;
276
+ const showInteractionsInSummary = !isOpen && pendingInteractions.length > 0;
277
+
196
278
  return (
197
279
  <div className='theia-delegation-container'>
198
- <details className='delegation-response-details'>
280
+ <details className='delegation-response-details' onToggle={this.handleToggle}>
199
281
  <summary className='delegation-summary'>
200
282
  <div className='delegation-header'>
201
283
  <span className='delegation-agent'>
202
284
  <span className='codicon codicon-copilot-large' /> {agentName}
203
285
  </span>
204
286
  <span className='delegation-status'>
287
+ {showInteractionsInSummary && (
288
+ <span className='delegation-interaction-badge' title={nls.localize(
289
+ 'theia/ai/chat-ui/delegation-response-renderer/interactionNeeded',
290
+ 'User interaction needed'
291
+ )}>
292
+ <span className='codicon codicon-warning'></span>
293
+ </span>
294
+ )}
205
295
  <span className={`codicon ${statusIcon} delegation-status-icon`}></span>
206
296
  <span className='delegation-status-text'>{statusText}</span>
207
297
  </span>
298
+ <span className={`delegation-toggle-arrow${isOpen ? ' open' : ''}`} />
208
299
  </div>
300
+ {showInteractionsInSummary && (
301
+ <div className='delegation-pending-confirmations'>
302
+ {pendingInteractions.map(({ contentPart, id }) =>
303
+ this.renderInteractionConfirmation(contentPart, id)
304
+ )}
305
+ </div>
306
+ )}
209
307
  </summary>
210
308
  <div className='delegation-content'>
211
309
  <div className='delegation-prompt-section'>
@@ -26,3 +26,4 @@ export * from './thinking-part-renderer';
26
26
  export * from './progress-part-renderer';
27
27
  export * from './tool-confirmation';
28
28
  export * from './delegation-tool-renderer';
29
+ export * from './tool-call-rendering';
@@ -15,17 +15,20 @@
15
15
  // *****************************************************************************
16
16
  import { ChatResponseContent, MultiSelectQuestionResponseHandler, QuestionResponseContent, QuestionResponseHandler } from '@theia/ai-chat';
17
17
  import { nls } from '@theia/core';
18
- import { codicon } from '@theia/core/lib/browser';
19
- import { injectable } from '@theia/core/shared/inversify';
18
+ import { codicon, OpenerService } from '@theia/core/lib/browser';
19
+ import { inject, injectable } from '@theia/core/shared/inversify';
20
20
  import * as React from '@theia/core/shared/react';
21
21
  import { ReactNode } from '@theia/core/shared/react';
22
22
  import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
23
23
  import { ResponseNode } from '../chat-tree-view';
24
+ import { useMarkdownRendering } from './markdown-part-renderer';
24
25
 
25
26
  @injectable()
26
27
  export class QuestionPartRenderer
27
28
  implements ChatResponsePartRenderer<QuestionResponseContent> {
28
29
 
30
+ @inject(OpenerService) protected readonly openerService: OpenerService;
31
+
29
32
  canHandle(response: ChatResponseContent): number {
30
33
  if (QuestionResponseContent.is(response)) {
31
34
  return 10;
@@ -35,9 +38,16 @@ export class QuestionPartRenderer
35
38
 
36
39
  render(question: QuestionResponseContent, node: ResponseNode): ReactNode {
37
40
  if (question.multiSelect) {
38
- return <MultiSelectQuestion question={question} node={node} />;
41
+ return <MultiSelectQuestion question={question} node={node} openerService={this.openerService} />;
42
+ }
43
+ return <SingleSelectQuestion question={question} node={node} openerService={this.openerService} />;
44
+ }
45
+
46
+ renderConfirmation(question: QuestionResponseContent, node: ResponseNode): ReactNode {
47
+ if (question.multiSelect) {
48
+ return <MultiSelectQuestion question={question} node={node} openerService={this.openerService} />;
39
49
  }
40
- return <SingleSelectQuestion question={question} node={node} />;
50
+ return <SingleSelectQuestion question={question} node={node} openerService={this.openerService} />;
41
51
  }
42
52
 
43
53
  }
@@ -77,15 +87,18 @@ function DismissButton({ question, disabled }: { question: QuestionResponseConte
77
87
  );
78
88
  }
79
89
 
80
- function SingleSelectQuestion({ question, node }: { question: QuestionResponseContent, node: ResponseNode }): React.JSX.Element {
90
+ function SingleSelectQuestion({ question, node, openerService }: {
91
+ question: QuestionResponseContent, node: ResponseNode, openerService: OpenerService
92
+ }): React.JSX.Element {
81
93
  const isDisabled = question.isReadOnly || isResolved(question) || !node.response.isWaitingForInput;
82
94
  const hasDescriptions = question.options.some(option => option.description);
95
+ const questionRef = useMarkdownRendering(question.question, openerService);
83
96
 
84
97
  return (
85
98
  <div className="theia-QuestionPartRenderer-root">
86
99
  {question.onSkip && <DismissButton question={question} disabled={isDisabled} />}
87
100
  {question.header && <div className="theia-QuestionPartRenderer-header">{question.header}</div>}
88
- <div className="theia-QuestionPartRenderer-question">{question.question}</div>
101
+ <div className="theia-QuestionPartRenderer-question" ref={questionRef} />
89
102
  <div className={`theia-QuestionPartRenderer-options ${hasDescriptions ? 'has-descriptions' : ''}`}>
90
103
  {
91
104
  question.options.map((option, index) => (
@@ -112,7 +125,10 @@ function SingleSelectQuestion({ question, node }: { question: QuestionResponseCo
112
125
  );
113
126
  }
114
127
 
115
- function MultiSelectQuestion({ question, node }: { question: QuestionResponseContent, node: ResponseNode }): React.JSX.Element {
128
+ function MultiSelectQuestion({ question, node, openerService }: {
129
+ question: QuestionResponseContent, node: ResponseNode, openerService: OpenerService
130
+ }): React.JSX.Element {
131
+ const questionRef = useMarkdownRendering(question.question, openerService);
116
132
  const restoredIndices = React.useMemo(() => {
117
133
  if (question.selectedOptions && question.selectedOptions.length > 0) {
118
134
  const indices = new Set<number>();
@@ -165,7 +181,7 @@ function MultiSelectQuestion({ question, node }: { question: QuestionResponseCon
165
181
  <div className="theia-QuestionPartRenderer-root">
166
182
  <DismissButton question={question} disabled={isDisabled} />
167
183
  {question.header && <div className="theia-QuestionPartRenderer-header">{question.header}</div>}
168
- <div className="theia-QuestionPartRenderer-question">{question.question}</div>
184
+ <div className="theia-QuestionPartRenderer-question" ref={questionRef} />
169
185
  <div className={`theia-QuestionPartRenderer-options ${hasDescriptions ? 'has-descriptions' : ''}`}>
170
186
  {question.options.map((option, index) => (
171
187
  <button
@@ -0,0 +1,89 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 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';
19
+ import { codicon } from '@theia/core/lib/browser';
20
+ import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
21
+
22
+ export interface CopyButtonProps {
23
+ text: string;
24
+ clipboardService: ClipboardService;
25
+ }
26
+
27
+ export const CopyButton: React.FC<CopyButtonProps> = ({ text, clipboardService }) => {
28
+ const handleCopy = React.useCallback(() => {
29
+ clipboardService.writeText(text);
30
+ }, [text, clipboardService]);
31
+
32
+ return (
33
+ <button
34
+ className="tool-call copy-button"
35
+ onClick={handleCopy}
36
+ title={nls.localizeByDefault('Copy')}
37
+ >
38
+ <span className={codicon('copy')} />
39
+ </button>
40
+ );
41
+ };
42
+
43
+ export interface MetaRowProps {
44
+ icon: string;
45
+ label: string;
46
+ children: React.ReactNode;
47
+ }
48
+
49
+ export const MetaRow: React.FC<MetaRowProps> = ({ icon, label, children }) => (
50
+ <div className="tool-call meta-row" title={label}>
51
+ <span className={codicon(icon)} />
52
+ <span>{children}</span>
53
+ </div>
54
+ );
55
+
56
+ export interface OutputBoxProps {
57
+ title: string;
58
+ output?: string;
59
+ clipboardService: ClipboardService;
60
+ }
61
+
62
+ export const OutputBox: React.FC<OutputBoxProps> = ({ title, output, clipboardService }) => (
63
+ <div className="tool-call output-box">
64
+ <div className="tool-call output-header">
65
+ <span className={codicon('output')} />
66
+ {title}
67
+ {output && <CopyButton text={output} clipboardService={clipboardService} />}
68
+ </div>
69
+ {output ? (
70
+ <pre className="tool-call output">{output}</pre>
71
+ ) : (
72
+ <div className="tool-call no-output">
73
+ {nls.localize('theia/ai-chat-ui/noOutput', 'No output')}
74
+ </div>
75
+ )}
76
+ </div>
77
+ );
78
+
79
+ export function formatDuration(ms: number): string {
80
+ if (ms < 1000) {
81
+ return `${ms}ms`;
82
+ }
83
+ if (ms < 60000) {
84
+ return `${(ms / 1000).toFixed(1)}s`;
85
+ }
86
+ const minutes = Math.floor(ms / 60000);
87
+ const seconds = Math.floor((ms % 60000) / 1000);
88
+ return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
89
+ }
@@ -126,6 +126,32 @@ export function useToolConfirmationState(
126
126
  return { confirmationState, rejectionReason };
127
127
  }
128
128
 
129
+ export function createConfirmationHandlers(
130
+ toolId: string | undefined,
131
+ response: ToolCallChatResponseContent,
132
+ toolConfirmationManager: ToolConfirmationManager,
133
+ chatId: string,
134
+ toolRequest?: ToolRequest
135
+ ): { handleAllow: (scope: ConfirmationScope) => void; handleDeny: (scope: ConfirmationScope, reason?: string) => void } {
136
+ const handleAllow = (scope: ConfirmationScope): void => {
137
+ if (scope === 'forever' && toolId) {
138
+ toolConfirmationManager.setConfirmationMode(toolId, ToolConfirmationPreferenceMode.ALWAYS_ALLOW, toolRequest);
139
+ } else if (scope === 'session' && toolId) {
140
+ toolConfirmationManager.setSessionConfirmationMode(toolId, ToolConfirmationPreferenceMode.ALWAYS_ALLOW, chatId);
141
+ }
142
+ response.confirm();
143
+ };
144
+ const handleDeny = (scope: ConfirmationScope, reason?: string): void => {
145
+ if (scope === 'forever' && toolId) {
146
+ toolConfirmationManager.setConfirmationMode(toolId, ToolConfirmationPreferenceMode.DISABLED);
147
+ } else if (scope === 'session' && toolId) {
148
+ toolConfirmationManager.setSessionConfirmationMode(toolId, ToolConfirmationPreferenceMode.DISABLED, chatId);
149
+ }
150
+ response.deny(reason);
151
+ };
152
+ return { handleAllow, handleDeny };
153
+ }
154
+
129
155
  export interface ToolConfirmationCallbacks {
130
156
  toolRequest?: ToolRequest;
131
157
  onAllow: (scope: ConfirmationScope) => void;
@@ -21,7 +21,7 @@ import { ReactNode } from '@theia/core/shared/react';
21
21
  import { nls } from '@theia/core/lib/common/nls';
22
22
  import { codicon, ContextMenuRenderer, HoverService, OpenerService } from '@theia/core/lib/browser';
23
23
  import * as React from '@theia/core/shared/react';
24
- import { ToolConfirmation, useToolConfirmationState } from './tool-confirmation';
24
+ import { createConfirmationHandlers, ToolConfirmation, useToolConfirmationState } 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';
@@ -54,6 +54,22 @@ export class ToolCallPartRenderer implements ChatResponsePartRenderer<ToolCallCh
54
54
  return -1;
55
55
  }
56
56
 
57
+ renderConfirmation(response: ToolCallChatResponseContent, parentNode: ResponseNode): ReactNode {
58
+ const chatId = parentNode.sessionId;
59
+ const toolRequest = response.name ? this.toolInvocationRegistry.getFunction(response.name) : undefined;
60
+ const { handleAllow, handleDeny } = createConfirmationHandlers(
61
+ response.name, response, this.toolConfirmationManager, chatId, toolRequest
62
+ );
63
+
64
+ return <ToolConfirmation
65
+ response={response}
66
+ toolRequest={toolRequest}
67
+ onAllow={handleAllow}
68
+ onDeny={handleDeny}
69
+ contextMenuRenderer={this.contextMenuRenderer}
70
+ />;
71
+ }
72
+
57
73
  render(response: ToolCallChatResponseContent, parentNode: ResponseNode): ReactNode {
58
74
  const chatId = parentNode.sessionId;
59
75
  const toolRequest = response.name ? this.toolInvocationRegistry.getFunction(response.name) : undefined;
@@ -210,23 +226,10 @@ const ToolCallContent: React.FC<ToolCallContentProps> = ({
210
226
  }
211
227
  };
212
228
 
213
- const handleAllow = React.useCallback((mode: 'once' | 'session' | 'forever' = 'once') => {
214
- if (mode === 'forever' && response.name) {
215
- toolConfirmationManager.setConfirmationMode(response.name, ToolConfirmationMode.ALWAYS_ALLOW, toolRequest);
216
- } else if (mode === 'session' && response.name) {
217
- toolConfirmationManager.setSessionConfirmationMode(response.name, ToolConfirmationMode.ALWAYS_ALLOW, chatId);
218
- }
219
- response.confirm();
220
- }, [response, toolConfirmationManager, chatId, toolRequest]);
221
-
222
- const handleDeny = React.useCallback((mode: 'once' | 'session' | 'forever' = 'once', reason?: string) => {
223
- if (mode === 'forever' && response.name) {
224
- toolConfirmationManager.setConfirmationMode(response.name, ToolConfirmationMode.DISABLED);
225
- } else if (mode === 'session' && response.name) {
226
- toolConfirmationManager.setSessionConfirmationMode(response.name, ToolConfirmationMode.DISABLED, chatId);
227
- }
228
- response.deny(reason);
229
- }, [response, toolConfirmationManager, chatId]);
229
+ const { handleAllow, handleDeny } = React.useMemo(
230
+ () => createConfirmationHandlers(response.name, response, toolConfirmationManager, chatId, toolRequest),
231
+ [response, toolConfirmationManager, chatId, toolRequest]
232
+ );
230
233
 
231
234
  const reasonText = formatReason(rejectionReason);
232
235
 
@@ -16,6 +16,8 @@
16
16
 
17
17
  import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering/markdown-string';
18
18
 
19
+ export { extractJsonStringField } from '../../common/toolcall-utils';
20
+
19
21
  const SHORT_STRING_THRESHOLD = 50;
20
22
  const MAX_CONDENSED_LENGTH = 80;
21
23
  const MAX_CONDENSED_VALUE_LENGTH = 30;