@theia/ai-ide 1.76.0-next.6 → 1.76.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 (174) hide show
  1. package/lib/browser/ai-allow-all-mode-chat-banner.js +1 -1
  2. package/lib/browser/ai-allow-all-mode-chat-banner.js.map +1 -1
  3. package/lib/browser/ai-aware-scm-commit-widget.d.ts +27 -0
  4. package/lib/browser/ai-aware-scm-commit-widget.d.ts.map +1 -0
  5. package/lib/browser/ai-aware-scm-commit-widget.js +142 -0
  6. package/lib/browser/ai-aware-scm-commit-widget.js.map +1 -0
  7. package/lib/browser/ai-configuration/ai-configuration-detail-widget.d.ts +10 -4
  8. package/lib/browser/ai-configuration/ai-configuration-detail-widget.d.ts.map +1 -1
  9. package/lib/browser/ai-configuration/ai-configuration-detail-widget.js +12 -7
  10. package/lib/browser/ai-configuration/ai-configuration-detail-widget.js.map +1 -1
  11. package/lib/browser/ai-configuration/ai-configuration-detail-widget.spec.js +14 -14
  12. package/lib/browser/ai-configuration/ai-configuration-detail-widget.spec.js.map +1 -1
  13. package/lib/browser/ai-configuration/ai-configuration-tree-widget.d.ts +6 -0
  14. package/lib/browser/ai-configuration/ai-configuration-tree-widget.d.ts.map +1 -1
  15. package/lib/browser/ai-configuration/ai-configuration-tree-widget.js +9 -0
  16. package/lib/browser/ai-configuration/ai-configuration-tree-widget.js.map +1 -1
  17. package/lib/browser/ai-configuration/categories/agent-capabilities-settings.d.ts.map +1 -1
  18. package/lib/browser/ai-configuration/categories/agent-capabilities-settings.js +2 -1
  19. package/lib/browser/ai-configuration/categories/agent-capabilities-settings.js.map +1 -1
  20. package/lib/browser/ai-configuration/categories/agent-chips.js +1 -1
  21. package/lib/browser/ai-configuration/categories/agent-chips.js.map +1 -1
  22. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.d.ts +8 -0
  23. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.d.ts.map +1 -1
  24. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.js +27 -8
  25. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.js.map +1 -1
  26. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.spec.js +38 -2
  27. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.spec.js.map +1 -1
  28. package/lib/browser/ai-configuration/categories/models-configuration-category.d.ts +13 -2
  29. package/lib/browser/ai-configuration/categories/models-configuration-category.d.ts.map +1 -1
  30. package/lib/browser/ai-configuration/categories/models-configuration-category.js +41 -2
  31. package/lib/browser/ai-configuration/categories/models-configuration-category.js.map +1 -1
  32. package/lib/browser/ai-configuration/categories/models-configuration-category.spec.js +32 -2
  33. package/lib/browser/ai-configuration/categories/models-configuration-category.spec.js.map +1 -1
  34. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.d.ts +26 -0
  35. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.d.ts.map +1 -0
  36. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.js +212 -0
  37. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.js.map +1 -0
  38. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.d.ts +2 -0
  39. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.d.ts.map +1 -0
  40. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.js +423 -0
  41. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.js.map +1 -0
  42. package/lib/browser/ai-configuration/language-model-renderer.d.ts.map +1 -1
  43. package/lib/browser/ai-configuration/language-model-renderer.js +10 -3
  44. package/lib/browser/ai-configuration/language-model-renderer.js.map +1 -1
  45. package/lib/browser/ai-getting-started-commands.d.ts +10 -0
  46. package/lib/browser/ai-getting-started-commands.d.ts.map +1 -0
  47. package/lib/browser/ai-getting-started-commands.js +34 -0
  48. package/lib/browser/ai-getting-started-commands.js.map +1 -0
  49. package/lib/browser/ai-getting-started-contribution.d.ts +20 -0
  50. package/lib/browser/ai-getting-started-contribution.d.ts.map +1 -0
  51. package/lib/browser/ai-getting-started-contribution.js +70 -0
  52. package/lib/browser/ai-getting-started-contribution.js.map +1 -0
  53. package/lib/browser/ai-getting-started-walkthrough.d.ts +41 -0
  54. package/lib/browser/ai-getting-started-walkthrough.d.ts.map +1 -0
  55. package/lib/browser/ai-getting-started-walkthrough.js +225 -0
  56. package/lib/browser/ai-getting-started-walkthrough.js.map +1 -0
  57. package/lib/browser/ai-getting-started-walkthrough.spec.d.ts +2 -0
  58. package/lib/browser/ai-getting-started-walkthrough.spec.d.ts.map +1 -0
  59. package/lib/browser/ai-getting-started-walkthrough.spec.js +124 -0
  60. package/lib/browser/ai-getting-started-walkthrough.spec.js.map +1 -0
  61. package/lib/browser/ai-walkthrough-context-keys.d.ts +55 -0
  62. package/lib/browser/ai-walkthrough-context-keys.d.ts.map +1 -0
  63. package/lib/browser/ai-walkthrough-context-keys.js +145 -0
  64. package/lib/browser/ai-walkthrough-context-keys.js.map +1 -0
  65. package/lib/browser/ai-walkthrough-context-keys.spec.d.ts +2 -0
  66. package/lib/browser/ai-walkthrough-context-keys.spec.d.ts.map +1 -0
  67. package/lib/browser/ai-walkthrough-context-keys.spec.js +166 -0
  68. package/lib/browser/ai-walkthrough-context-keys.spec.js.map +1 -0
  69. package/lib/browser/chat-session-item.js +2 -2
  70. package/lib/browser/chat-session-item.js.map +1 -1
  71. package/lib/browser/commit-message-agent.d.ts +31 -0
  72. package/lib/browser/commit-message-agent.d.ts.map +1 -0
  73. package/lib/browser/commit-message-agent.js +99 -0
  74. package/lib/browser/commit-message-agent.js.map +1 -0
  75. package/lib/browser/commit-message-agent.spec.d.ts +2 -0
  76. package/lib/browser/commit-message-agent.spec.d.ts.map +1 -0
  77. package/lib/browser/commit-message-agent.spec.js +82 -0
  78. package/lib/browser/commit-message-agent.spec.js.map +1 -0
  79. package/lib/browser/commit-message-command-contribution.d.ts +15 -0
  80. package/lib/browser/commit-message-command-contribution.d.ts.map +1 -0
  81. package/lib/browser/commit-message-command-contribution.js +73 -0
  82. package/lib/browser/commit-message-command-contribution.js.map +1 -0
  83. package/lib/browser/commit-message-commands.d.ts +5 -0
  84. package/lib/browser/commit-message-commands.d.ts.map +1 -0
  85. package/lib/browser/commit-message-commands.js +28 -0
  86. package/lib/browser/commit-message-commands.js.map +1 -0
  87. package/lib/browser/commit-message-prompt-template.d.ts +11 -0
  88. package/lib/browser/commit-message-prompt-template.d.ts.map +1 -0
  89. package/lib/browser/commit-message-prompt-template.js +63 -0
  90. package/lib/browser/commit-message-prompt-template.js.map +1 -0
  91. package/lib/browser/commit-message-runner.d.ts +41 -0
  92. package/lib/browser/commit-message-runner.d.ts.map +1 -0
  93. package/lib/browser/commit-message-runner.js +143 -0
  94. package/lib/browser/commit-message-runner.js.map +1 -0
  95. package/lib/browser/commit-message-runner.spec.d.ts +2 -0
  96. package/lib/browser/commit-message-runner.spec.d.ts.map +1 -0
  97. package/lib/browser/commit-message-runner.spec.js +206 -0
  98. package/lib/browser/commit-message-runner.spec.js.map +1 -0
  99. package/lib/browser/frontend-module.d.ts.map +1 -1
  100. package/lib/browser/frontend-module.js +25 -0
  101. package/lib/browser/frontend-module.js.map +1 -1
  102. package/lib/browser/git-changes-tool.d.ts +73 -0
  103. package/lib/browser/git-changes-tool.d.ts.map +1 -0
  104. package/lib/browser/git-changes-tool.js +217 -0
  105. package/lib/browser/git-changes-tool.js.map +1 -0
  106. package/lib/browser/git-changes-tool.spec.d.ts +2 -0
  107. package/lib/browser/git-changes-tool.spec.d.ts.map +1 -0
  108. package/lib/browser/git-changes-tool.spec.js +167 -0
  109. package/lib/browser/git-changes-tool.spec.js.map +1 -0
  110. package/lib/browser/ide-chat-welcome-message-provider.d.ts +26 -28
  111. package/lib/browser/ide-chat-welcome-message-provider.d.ts.map +1 -1
  112. package/lib/browser/ide-chat-welcome-message-provider.js +61 -151
  113. package/lib/browser/ide-chat-welcome-message-provider.js.map +1 -1
  114. package/lib/browser/review/pr-review-capability-contribution.js +1 -1
  115. package/lib/browser/review/pr-review-capability-contribution.js.map +1 -1
  116. package/lib/browser/user-interaction-tool-renderer.d.ts +6 -0
  117. package/lib/browser/user-interaction-tool-renderer.d.ts.map +1 -1
  118. package/lib/browser/user-interaction-tool-renderer.js +36 -7
  119. package/lib/browser/user-interaction-tool-renderer.js.map +1 -1
  120. package/lib/browser/user-interaction-tool.d.ts +23 -1
  121. package/lib/browser/user-interaction-tool.d.ts.map +1 -1
  122. package/lib/browser/user-interaction-tool.js +56 -17
  123. package/lib/browser/user-interaction-tool.js.map +1 -1
  124. package/lib/browser/user-interaction-tool.spec.js +151 -2
  125. package/lib/browser/user-interaction-tool.spec.js.map +1 -1
  126. package/lib/common/command-prompt-template.js +1 -1
  127. package/lib/common/user-interaction-tool.d.ts +19 -1
  128. package/lib/common/user-interaction-tool.d.ts.map +1 -1
  129. package/lib/common/user-interaction-tool.js +100 -29
  130. package/lib/common/user-interaction-tool.js.map +1 -1
  131. package/lib/common/user-interaction-tool.spec.js +215 -174
  132. package/lib/common/user-interaction-tool.spec.js.map +1 -1
  133. package/package.json +26 -25
  134. package/src/browser/ai-allow-all-mode-chat-banner.tsx +2 -2
  135. package/src/browser/ai-aware-scm-commit-widget.tsx +153 -0
  136. package/src/browser/ai-configuration/ai-configuration-detail-widget.spec.ts +14 -14
  137. package/src/browser/ai-configuration/ai-configuration-detail-widget.tsx +12 -7
  138. package/src/browser/ai-configuration/ai-configuration-tree-widget.tsx +8 -0
  139. package/src/browser/ai-configuration/categories/agent-capabilities-settings.tsx +7 -8
  140. package/src/browser/ai-configuration/categories/agent-chips.tsx +1 -1
  141. package/src/browser/ai-configuration/categories/model-aliases-configuration-category.spec.ts +40 -2
  142. package/src/browser/ai-configuration/categories/model-aliases-configuration-category.tsx +31 -10
  143. package/src/browser/ai-configuration/categories/models-configuration-category.spec.ts +47 -2
  144. package/src/browser/ai-configuration/categories/models-configuration-category.tsx +52 -5
  145. package/src/browser/ai-configuration/categories/provider-model-discovery-section.spec.tsx +451 -0
  146. package/src/browser/ai-configuration/categories/provider-model-discovery-section.tsx +341 -0
  147. package/src/browser/ai-configuration/language-model-renderer.tsx +11 -3
  148. package/src/browser/ai-getting-started-commands.ts +33 -0
  149. package/src/browser/ai-getting-started-contribution.ts +64 -0
  150. package/src/browser/ai-getting-started-walkthrough.spec.ts +146 -0
  151. package/src/browser/ai-getting-started-walkthrough.ts +249 -0
  152. package/src/browser/ai-walkthrough-context-keys.spec.ts +209 -0
  153. package/src/browser/ai-walkthrough-context-keys.ts +147 -0
  154. package/src/browser/chat-session-item.tsx +2 -2
  155. package/src/browser/commit-message-agent.spec.ts +97 -0
  156. package/src/browser/commit-message-agent.ts +113 -0
  157. package/src/browser/commit-message-command-contribution.ts +67 -0
  158. package/src/browser/commit-message-commands.ts +25 -0
  159. package/src/browser/commit-message-prompt-template.ts +65 -0
  160. package/src/browser/commit-message-runner.spec.ts +266 -0
  161. package/src/browser/commit-message-runner.ts +163 -0
  162. package/src/browser/frontend-module.ts +27 -0
  163. package/src/browser/git-changes-tool.spec.ts +200 -0
  164. package/src/browser/git-changes-tool.ts +239 -0
  165. package/src/browser/ide-chat-welcome-message-provider.tsx +93 -230
  166. package/src/browser/review/pr-review-capability-contribution.ts +1 -1
  167. package/src/browser/style/commit-message-overlay.css +64 -0
  168. package/src/browser/style/index.css +4 -42
  169. package/src/browser/user-interaction-tool-renderer.tsx +38 -8
  170. package/src/browser/user-interaction-tool.spec.ts +201 -2
  171. package/src/browser/user-interaction-tool.ts +69 -18
  172. package/src/common/command-prompt-template.ts +1 -1
  173. package/src/common/user-interaction-tool.spec.ts +217 -161
  174. package/src/common/user-interaction-tool.ts +127 -32
@@ -81,7 +81,9 @@ const UserInteractionComponent: React.FC<UserInteractionComponentProps> = ({
81
81
  }) => {
82
82
  const steps = args.interactions;
83
83
  const stepCount = steps.length;
84
- const [currentStep, setCurrentStep] = React.useState(0);
84
+ // Resume at the step another mount of this interaction left off at (e.g. the collapsed
85
+ // delegation summary vs. the expanded details render independent components).
86
+ const [currentStep, setCurrentStep] = React.useState(() => tool.getCurrentStep(toolCallId) ?? 0);
85
87
  // The tool's result (partial or final) is the single source of truth for step states.
86
88
  const [stepStates, setStepStates] = React.useState<StepState[]>(() => {
87
89
  if (result) {
@@ -122,6 +124,13 @@ const UserInteractionComponent: React.FC<UserInteractionComponentProps> = ({
122
124
  }
123
125
  }, [currentStep, activeStep, isFinal, tool]);
124
126
 
127
+ // Keep the tool informed of the current step so that a later mount resumes here.
128
+ React.useEffect(() => {
129
+ if (!isFinal) {
130
+ tool.recordCurrentStep(toolCallId, currentStep);
131
+ }
132
+ }, [currentStep, isFinal, tool, toolCallId]);
133
+
125
134
  const buildResult = React.useCallback((completed: boolean, states: StepState[]): UserInteractionResult => ({
126
135
  completed,
127
136
  steps: steps.map((step, i) => {
@@ -494,8 +503,13 @@ const MalformedInteraction: React.FC<{ message: string }> = ({ message }) => (
494
503
  </div>
495
504
  );
496
505
 
497
- interface ToolErrorResult { error: string }
498
- function parseToolErrorResult(raw: unknown): ToolErrorResult | undefined {
506
+ /**
507
+ * Extract the message to show for a tool result that the renderer cannot turn into an interaction.
508
+ * Covers both shapes the tool can produce for malformed arguments: the `{ error }` result of the
509
+ * handler, and the `{ denied: true, reason }` result of the auto-deny in `checkAutoAction`, which
510
+ * is what a validation failure produces before the handler ever runs.
511
+ */
512
+ function extractErrorMessage(raw: unknown): string | undefined {
499
513
  let candidate: unknown = raw;
500
514
  if (typeof raw === 'string') {
501
515
  try {
@@ -504,8 +518,15 @@ function parseToolErrorResult(raw: unknown): ToolErrorResult | undefined {
504
518
  return undefined;
505
519
  }
506
520
  }
507
- if (candidate && typeof candidate === 'object' && typeof (candidate as { error?: unknown }).error === 'string') {
508
- return candidate as ToolErrorResult;
521
+ if (!candidate || typeof candidate !== 'object') {
522
+ return undefined;
523
+ }
524
+ const error = (candidate as { error?: unknown }).error;
525
+ if (typeof error === 'string') {
526
+ return error;
527
+ }
528
+ if (ToolCallChatResponseContent.isDenialResult(candidate) && typeof candidate.reason === 'string') {
529
+ return candidate.reason;
509
530
  }
510
531
  return undefined;
511
532
  }
@@ -553,8 +574,18 @@ export class UserInteractionToolRenderer implements ChatResponsePartRenderer<Too
553
574
  return -1;
554
575
  }
555
576
 
577
+ /**
578
+ * The regular rendering of this tool already is the interactive UI, so reuse it
579
+ * where only the pending interaction should be shown (e.g. on the collapsed
580
+ * summary of a delegated session, see #17952).
581
+ */
582
+ renderConfirmation(response: ToolCallChatResponseContent, parentNode: ResponseNode): ReactNode {
583
+ return this.render(response, parentNode);
584
+ }
585
+
556
586
  render(response: ToolCallChatResponseContent, parentNode: ResponseNode): ReactNode {
557
- const args = parseUserInteractionArgs(response.arguments);
587
+ const validation = parseUserInteractionArgs(response.arguments);
588
+ const args = validation.ok ? validation.args : undefined;
558
589
 
559
590
  if (!args || !response.id) {
560
591
  // The tool already returned a result but the args don't validate: this
@@ -562,8 +593,7 @@ export class UserInteractionToolRenderer implements ChatResponsePartRenderer<Too
562
593
  // rejected, or arguments that fail shared parsing). Show an error state
563
594
  // instead of a perpetual loading spinner.
564
595
  if (response.result !== undefined) {
565
- const error = parseToolErrorResult(response.result);
566
- const message = error?.error
596
+ const message = extractErrorMessage(response.result)
567
597
  ?? nls.localize('theia/ai-ide/userInteractionMalformedFallback', 'The arguments could not be parsed.');
568
598
  return <MalformedInteraction message={message} />;
569
599
  }
@@ -119,13 +119,73 @@ describe('UserInteractionTool', () => {
119
119
  it('should return error when no interactions are provided', async () => {
120
120
  const handler = tool.getTool().handler;
121
121
  const result = await handler(JSON.stringify({ interactions: [] }), { toolCallId: 'x' });
122
- expect(JSON.parse(result as string).error).to.equal('No interactions provided');
122
+ expect(JSON.parse(result as string).error).to.match(/at least one step/);
123
123
  });
124
124
 
125
125
  it('should return error when arguments are invalid JSON', async () => {
126
126
  const handler = tool.getTool().handler;
127
127
  const result = await handler('not-json', { toolCallId: 'x' });
128
- expect(JSON.parse(result as string).error).to.equal('Invalid arguments');
128
+ expect(JSON.parse(result as string).error).to.match(/valid JSON/i);
129
+ });
130
+
131
+ it('should reject a single step whose options are malformed instead of auto-completing it', async () => {
132
+ const handler = tool.getTool().handler;
133
+ const args = JSON.stringify({
134
+ interactions: [{
135
+ title: 'Apply fix?',
136
+ message: 'Should I apply this change?',
137
+ options: '[{"text":"Yes","value":"yes"}]'
138
+ }]
139
+ });
140
+ const result = JSON.parse(await handler(args, { toolCallId: 'malformed-options' }) as string);
141
+ expect(result.error).to.match(/step 1: "options" must be an array/i);
142
+ expect(result.completed).to.be.undefined;
143
+ });
144
+
145
+ it('should reject a single step with a partially malformed options array instead of silently dropping the bad option', async () => {
146
+ const handler = tool.getTool().handler;
147
+ const args = JSON.stringify({
148
+ interactions: [{
149
+ title: 'Apply fix?',
150
+ message: 'Should I apply this change?',
151
+ options: [{ text: 'Approve', value: 'approve' }, { text: 'Reject', value: 'reject' }, { label: 'Defer', value: 'defer' }]
152
+ }]
153
+ });
154
+ const result = JSON.parse(await handler(args, { toolCallId: 'partial-options' }) as string);
155
+ expect(result.error).to.match(/step 1, option 3: "text" and "value" are required strings/i);
156
+ expect(result.completed).to.be.undefined;
157
+ });
158
+
159
+ it('should reject a JSON-encoded interactions string with an actionable message', async () => {
160
+ const handler = tool.getTool().handler;
161
+ const args = JSON.stringify({ interactions: '[{"title":"T","message":"M"}]' });
162
+ const result = JSON.parse(await handler(args, { toolCallId: 'encoded-interactions' }) as string);
163
+ expect(result.error).to.match(/"interactions" must be an array of step objects, received string/);
164
+ expect(result.error).to.match(/do not JSON-encode nested values/i);
165
+ });
166
+
167
+ it('should auto-complete a single step that genuinely has no options', async () => {
168
+ const handler = tool.getTool().handler;
169
+ const args = JSON.stringify({ interactions: [{ title: 'FYI', message: 'Heads up' }] });
170
+ const result = parseResult(await handler(args, { toolCallId: 'informational' }));
171
+ expect(result.completed).to.be.true;
172
+ expect(result.steps).to.deep.equal([{ title: 'FYI' }]);
173
+ });
174
+
175
+ describe('checkAutoAction', () => {
176
+
177
+ it('should auto-deny malformed arguments so the model receives the validation error', () => {
178
+ const args = JSON.stringify({
179
+ interactions: [{ title: 'Apply fix?', message: 'Confirm?', options: '[{"text":"Yes","value":"yes"}]' }]
180
+ });
181
+ const autoAction = tool.getTool().checkAutoAction!(args);
182
+ expect(autoAction?.action).to.equal('deny');
183
+ expect(autoAction?.reason).to.match(/step 1: "options" must be an array/i);
184
+ });
185
+
186
+ it('should not auto-deny well-formed arguments, leaving confirmation to the user', () => {
187
+ expect(tool.getTool().checkAutoAction!(singleStepArgs())).to.be.undefined;
188
+ });
129
189
  });
130
190
 
131
191
  it('should return error when no tool call ID is available', async () => {
@@ -134,6 +194,145 @@ describe('UserInteractionTool', () => {
134
194
  expect(JSON.parse(result as string).error).to.equal('No tool call ID available');
135
195
  });
136
196
 
197
+ describe('chat context surfacing', () => {
198
+ interface MockChatCtx {
199
+ ctx: object;
200
+ response: {
201
+ response: { content: object[] };
202
+ fireInteractionNeeded: sinon.SinonSpy;
203
+ waitForInput: sinon.SinonSpy;
204
+ stopWaitingForInput: sinon.SinonSpy;
205
+ };
206
+ contentPart: {
207
+ kind: string;
208
+ id: string;
209
+ requestUserInput: sinon.SinonSpy;
210
+ userInputHandled: sinon.SinonSpy;
211
+ };
212
+ }
213
+
214
+ // Mimics the shape of ChatToolContext with a mock response model, so we can
215
+ // assert the tool announces its pending interaction (issue #17952: interactions
216
+ // inside delegated sessions were never surfaced because the tool never fired
217
+ // interactionNeeded).
218
+ const createChatCtx = (toolCallId: string, withContentPart = true): MockChatCtx => {
219
+ const contentPart = {
220
+ kind: 'toolCall',
221
+ id: toolCallId,
222
+ requestUserInput: sinon.spy(),
223
+ userInputHandled: sinon.spy()
224
+ };
225
+ const response = {
226
+ response: { content: withContentPart ? [contentPart] : [] },
227
+ fireInteractionNeeded: sinon.spy(),
228
+ waitForInput: sinon.spy(),
229
+ stopWaitingForInput: sinon.spy()
230
+ };
231
+ return { ctx: { toolCallId, request: {}, response }, response, contentPart };
232
+ };
233
+
234
+ it('should fire interactionNeeded with the tool call content part while waiting', async () => {
235
+ const handler = tool.getTool().handler;
236
+ const { ctx, response, contentPart } = createChatCtx('call-fire');
237
+
238
+ const handlerPromise = handler(singleStepArgs(), ctx);
239
+
240
+ expect(response.fireInteractionNeeded.calledOnce).to.be.true;
241
+ expect(response.fireInteractionNeeded.firstCall.args[0]).to.equal(contentPart);
242
+ expect(response.waitForInput.calledOnce).to.be.true;
243
+
244
+ tool.completeInteraction('call-fire', { completed: true, steps: [{ title: 'Choose', value: 'a' }] });
245
+ await handlerPromise;
246
+ });
247
+
248
+ it('should mark the content part as awaiting user input while blocked and clear it afterwards', async () => {
249
+ const handler = tool.getTool().handler;
250
+ const { ctx, contentPart } = createChatCtx('call-mark');
251
+
252
+ const handlerPromise = handler(singleStepArgs(), ctx);
253
+
254
+ expect(contentPart.requestUserInput.calledOnce).to.be.true;
255
+ expect(contentPart.userInputHandled.called).to.be.false;
256
+
257
+ tool.completeInteraction('call-mark', { completed: true, steps: [{ title: 'Choose', value: 'a' }] });
258
+ await handlerPromise;
259
+
260
+ expect(contentPart.userInputHandled.calledOnce).to.be.true;
261
+ });
262
+
263
+ it('should not fire interactionNeeded when no matching tool call content exists', async () => {
264
+ const handler = tool.getTool().handler;
265
+ const { ctx, response } = createChatCtx('call-missing', false);
266
+
267
+ const handlerPromise = handler(singleStepArgs(), ctx);
268
+
269
+ expect(response.fireInteractionNeeded.called).to.be.false;
270
+ // Waiting state must still be surfaced even without a content part.
271
+ expect(response.waitForInput.calledOnce).to.be.true;
272
+
273
+ tool.completeInteraction('call-missing', { completed: true, steps: [{ title: 'Choose', value: 'a' }] });
274
+ await handlerPromise;
275
+ });
276
+
277
+ it('should not fire interactionNeeded for informational single-step interactions', async () => {
278
+ const handler = tool.getTool().handler;
279
+ const { ctx, response } = createChatCtx('call-info');
280
+
281
+ const result = parseResult(await handler(
282
+ JSON.stringify({ interactions: [{ title: 'FYI', message: 'Just info' }] }), ctx
283
+ ));
284
+
285
+ expect(result.completed).to.be.true;
286
+ expect(response.fireInteractionNeeded.called).to.be.false;
287
+ expect(response.waitForInput.called).to.be.false;
288
+ });
289
+ });
290
+
291
+ describe('current step across renderer mounts', () => {
292
+ // The wizard can be mounted twice for one interaction (collapsed delegation summary
293
+ // and expanded details). The tool keeps the current step so a new mount resumes
294
+ // where the previous one left off instead of restarting at step 0.
295
+ const multiStepArgs = (): string => JSON.stringify({
296
+ interactions: [
297
+ { title: 'One', message: 'first' },
298
+ { title: 'Two', message: 'second', options: [{ text: 'OK', value: 'ok' }] },
299
+ { title: 'Three', message: 'third' }
300
+ ]
301
+ });
302
+ const completedResult = (): UserInteractionResult => ({
303
+ completed: true,
304
+ steps: [{ title: 'One' }, { title: 'Two', value: 'ok' }, { title: 'Three' }]
305
+ });
306
+
307
+ it('should return undefined when no interaction is pending', () => {
308
+ expect(tool.getCurrentStep('unknown')).to.be.undefined;
309
+ });
310
+
311
+ it('should remember the recorded step while the interaction is pending', async () => {
312
+ const handler = tool.getTool().handler;
313
+ const handlerPromise = handler(multiStepArgs(), { toolCallId: 'call-step' });
314
+
315
+ tool.recordCurrentStep('call-step', 2);
316
+ expect(tool.getCurrentStep('call-step')).to.equal(2);
317
+
318
+ tool.completeInteraction('call-step', completedResult());
319
+ await handlerPromise;
320
+ });
321
+
322
+ it('should forget the step once the interaction resolved', async () => {
323
+ const handler = tool.getTool().handler;
324
+ const handlerPromise = handler(multiStepArgs(), { toolCallId: 'call-step-done' });
325
+ tool.recordCurrentStep('call-step-done', 1);
326
+
327
+ tool.completeInteraction('call-step-done', completedResult());
328
+ await handlerPromise;
329
+
330
+ expect(tool.getCurrentStep('call-step-done')).to.be.undefined;
331
+ tool.recordCurrentStep('call-step-done', 2);
332
+ expect(tool.getCurrentStep('call-step-done')).to.be.undefined;
333
+ });
334
+ });
335
+
137
336
  it('should resolve the handler with the result passed to completeInteraction', async () => {
138
337
  const handler = tool.getTool().handler;
139
338
  const handlerPromise = handler(singleStepArgs(), { toolCallId: 'call-1' });
@@ -15,8 +15,8 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  import { ToolProvider, ToolRequest, ToolRequestParameterProperty, ToolRequestParameters } from '@theia/ai-core';
18
- import { ToolInvocationContext } from '@theia/ai-core/lib/common/language-model';
19
- import { ChatToolContext } from '@theia/ai-chat';
18
+ import { AutoActionResult, ToolInvocationContext } from '@theia/ai-core/lib/common/language-model';
19
+ import { ChatToolContext, ToolCallChatResponseContent } from '@theia/ai-chat';
20
20
  import { DiffUris } from '@theia/core/lib/browser/diff-uris';
21
21
  import { open, OpenerService } from '@theia/core/lib/browser';
22
22
  import { Deferred } from '@theia/core/lib/common/promise-util';
@@ -51,6 +51,13 @@ interface PendingInteraction {
51
51
  * is canceled. If absent (no renderer mounted) the tool resolves with all steps skipped.
52
52
  */
53
53
  latestPartial?: UserInteractionResult;
54
+ /**
55
+ * Step the user is currently viewing, pushed by the renderer via
56
+ * {@link UserInteractionTool.recordCurrentStep}. Lets another mount of the same
57
+ * interaction (e.g. the collapsed delegation summary and the expanded details)
58
+ * resume at that step instead of restarting at the first one.
59
+ */
60
+ currentStep?: number;
54
61
  }
55
62
 
56
63
  // Schemas are module-level constants so they are built once at load time
@@ -103,8 +110,9 @@ const STEP_SCHEMA: ToolRequestParameterProperty = {
103
110
  },
104
111
  required: ['text', 'value']
105
112
  },
106
- description: 'Optional buttons offered to the user for this step. Omit for purely informational steps; '
107
- + 'a hardcoded "Next"/"Finish" button is always shown to advance.'
113
+ description: 'Buttons offered to the user for this step. '
114
+ + 'Omit it (or pass an empty array) only for purely informational steps: a step without options is never a confirmation, '
115
+ + 'so never use one to ask for approval. In a multi-step interaction a hardcoded "Next"/"Finish" button is always shown to advance.'
108
116
  },
109
117
  links: {
110
118
  type: 'array',
@@ -161,7 +169,8 @@ const TOOL_DESCRIPTION = 'Present an interactive user interaction. Each step has
161
169
  + 'The tool returns a JSON string with { "completed": boolean, "steps": [{ "title", "value"?, "comments"?, "skipped"? }] }. '
162
170
  + 'If the user cancels mid-interaction, the tool returns whatever has been collected so far with "completed": false. '
163
171
  + 'Use this to walk users through a series of pre-determined findings or decisions in a single tool call, '
164
- + 'or to surface a single message/diff that should be shown inline in the chat.';
172
+ + 'or to surface a single message/diff that should be shown inline in the chat. '
173
+ + 'Pass nested values as real JSON arrays and objects, never as JSON-encoded strings.';
165
174
 
166
175
  @injectable()
167
176
  export class UserInteractionTool implements ToolProvider {
@@ -194,10 +203,22 @@ export class UserInteractionTool implements ToolProvider {
194
203
  providerName: 'ai-ide',
195
204
  description: TOOL_DESCRIPTION,
196
205
  parameters: TOOL_PARAMETERS,
197
- handler: (argString: string, ctx) => this.handleInteraction(argString, ctx)
206
+ handler: (argString: string, ctx) => this.handleInteraction(argString, ctx),
207
+ checkAutoAction: (argString: string) => this.checkArguments(argString)
198
208
  };
199
209
  }
200
210
 
211
+ /**
212
+ * Auto-deny invocations whose arguments do not validate, handing the validation error back to
213
+ * the model. Without this the confirmation flow would stall: the renderer refuses to mount the
214
+ * interaction (and with it the Allow/Deny buttons) for arguments it cannot render, so a
215
+ * confirmation that nobody can give would be awaited indefinitely.
216
+ */
217
+ protected checkArguments(argString: string): AutoActionResult | undefined {
218
+ const validation = parseUserInteractionArgs(argString);
219
+ return validation.ok ? undefined : { action: 'deny', reason: validation.error };
220
+ }
221
+
201
222
  /**
202
223
  * Resolve the pending interaction with the given final result. The renderer is the single
203
224
  * source of truth for the collected user input and passes the full result here on Finish
@@ -220,6 +241,25 @@ export class UserInteractionTool implements ToolProvider {
220
241
  pending.latestPartial = partial;
221
242
  }
222
243
 
244
+ /**
245
+ * Remember the step the user is currently viewing so another mount of the same
246
+ * interaction resumes there. Calls for an unknown or already-resolved interaction
247
+ * are silently ignored.
248
+ */
249
+ recordCurrentStep(toolCallId: string, step: number): void {
250
+ const pending = this.pendingInteractions.get(toolCallId);
251
+ if (!pending || pending.resolved) {
252
+ return;
253
+ }
254
+ pending.currentStep = step;
255
+ }
256
+
257
+ /** The step last recorded via {@link recordCurrentStep}, or undefined if nothing is pending. */
258
+ getCurrentStep(toolCallId: string): number | undefined {
259
+ const pending = this.pendingInteractions.get(toolCallId);
260
+ return pending && !pending.resolved ? pending.currentStep : undefined;
261
+ }
262
+
223
263
  protected resolveInteraction(toolCallId: string, result: UserInteractionResult): void {
224
264
  const pending = this.pendingInteractions.get(toolCallId);
225
265
  if (!pending || pending.resolved) {
@@ -310,18 +350,14 @@ export class UserInteractionTool implements ToolProvider {
310
350
  }
311
351
 
312
352
  protected async handleInteraction(argString: string, ctx: ToolInvocationContext | undefined): Promise<string> {
313
- try {
314
- JSON.parse(argString);
315
- } catch {
316
- return JSON.stringify({ error: 'Invalid arguments' });
317
- }
318
353
  // Validate via the shared parser so the tool only ever waits for steps that
319
- // the renderer would actually render. Otherwise the agent could send a
320
- // step the UI filters out, leaving the tool blocked on input forever.
321
- const validated = parseUserInteractionArgs(argString);
322
- if (!validated || validated.interactions.length === 0) {
323
- return JSON.stringify({ error: 'No interactions provided' });
354
+ // the renderer would actually render, and so that anything the agent got
355
+ // wrong is reported back to it verbatim instead of being silently dropped.
356
+ const validation = parseUserInteractionArgs(argString);
357
+ if (!validation.ok) {
358
+ return JSON.stringify({ error: validation.error });
324
359
  }
360
+ const validated = validation.args;
325
361
 
326
362
  const toolCallId = ToolInvocationContext.getToolCallId(ctx);
327
363
  if (!toolCallId) {
@@ -353,12 +389,27 @@ export class UserInteractionTool implements ToolProvider {
353
389
 
354
390
  // Mark the response as waiting for input while the interaction is pending, so it is
355
391
  // surfaced consistently with agent questions and tool confirmations (e.g. in the
356
- // session overview and notifications).
392
+ // session overview and notifications). Announce the interaction via
393
+ // interactionNeeded so UIs that track pending interactions (e.g. the collapsed
394
+ // delegation summary) surface it like tool confirmations (#17952).
357
395
  const response = ChatToolContext.is(ctx) ? ctx.response : undefined;
358
- response?.waitForInput();
396
+ let contentPart: ToolCallChatResponseContent | undefined;
397
+ if (response) {
398
+ contentPart = response.response.content.findLast(
399
+ (part): part is ToolCallChatResponseContent => ToolCallChatResponseContent.is(part) && part.id === toolCallId
400
+ );
401
+ if (contentPart) {
402
+ contentPart.requestUserInput();
403
+ response.fireInteractionNeeded(contentPart);
404
+ } else {
405
+ this.logger.warn(`No tool call content found for '${toolCallId}'; the pending interaction cannot be announced to interaction trackers.`);
406
+ }
407
+ response.waitForInput();
408
+ }
359
409
  try {
360
410
  return await pending.deferred.promise;
361
411
  } finally {
412
+ contentPart?.userInputHandled();
362
413
  response?.stopWaitingForInput();
363
414
  cancellationListener?.dispose();
364
415
  this.pendingInteractions.delete(toolCallId);
@@ -51,7 +51,7 @@ when the user want to pass arguments to the command.
51
51
  {
52
52
  "type": "theia-command",
53
53
  "commandId": "preferences:open",
54
- "arguments": ["ai-features"]
54
+ "arguments": ["editor.fontSize"]
55
55
  }
56
56
  \`\`\`
57
57