@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.
- package/lib/browser/ai-allow-all-mode-chat-banner.js +1 -1
- package/lib/browser/ai-allow-all-mode-chat-banner.js.map +1 -1
- package/lib/browser/ai-aware-scm-commit-widget.d.ts +27 -0
- package/lib/browser/ai-aware-scm-commit-widget.d.ts.map +1 -0
- package/lib/browser/ai-aware-scm-commit-widget.js +142 -0
- package/lib/browser/ai-aware-scm-commit-widget.js.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-detail-widget.d.ts +10 -4
- package/lib/browser/ai-configuration/ai-configuration-detail-widget.d.ts.map +1 -1
- package/lib/browser/ai-configuration/ai-configuration-detail-widget.js +12 -7
- package/lib/browser/ai-configuration/ai-configuration-detail-widget.js.map +1 -1
- package/lib/browser/ai-configuration/ai-configuration-detail-widget.spec.js +14 -14
- package/lib/browser/ai-configuration/ai-configuration-detail-widget.spec.js.map +1 -1
- package/lib/browser/ai-configuration/ai-configuration-tree-widget.d.ts +6 -0
- package/lib/browser/ai-configuration/ai-configuration-tree-widget.d.ts.map +1 -1
- package/lib/browser/ai-configuration/ai-configuration-tree-widget.js +9 -0
- package/lib/browser/ai-configuration/ai-configuration-tree-widget.js.map +1 -1
- package/lib/browser/ai-configuration/categories/agent-capabilities-settings.d.ts.map +1 -1
- package/lib/browser/ai-configuration/categories/agent-capabilities-settings.js +2 -1
- package/lib/browser/ai-configuration/categories/agent-capabilities-settings.js.map +1 -1
- package/lib/browser/ai-configuration/categories/agent-chips.js +1 -1
- package/lib/browser/ai-configuration/categories/agent-chips.js.map +1 -1
- package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.d.ts +8 -0
- package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.d.ts.map +1 -1
- package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.js +27 -8
- package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.js.map +1 -1
- package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.spec.js +38 -2
- package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.spec.js.map +1 -1
- package/lib/browser/ai-configuration/categories/models-configuration-category.d.ts +13 -2
- package/lib/browser/ai-configuration/categories/models-configuration-category.d.ts.map +1 -1
- package/lib/browser/ai-configuration/categories/models-configuration-category.js +41 -2
- package/lib/browser/ai-configuration/categories/models-configuration-category.js.map +1 -1
- package/lib/browser/ai-configuration/categories/models-configuration-category.spec.js +32 -2
- package/lib/browser/ai-configuration/categories/models-configuration-category.spec.js.map +1 -1
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.d.ts +26 -0
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.d.ts.map +1 -0
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.js +212 -0
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.js.map +1 -0
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.d.ts +2 -0
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.d.ts.map +1 -0
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.js +423 -0
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.js.map +1 -0
- package/lib/browser/ai-configuration/language-model-renderer.d.ts.map +1 -1
- package/lib/browser/ai-configuration/language-model-renderer.js +10 -3
- package/lib/browser/ai-configuration/language-model-renderer.js.map +1 -1
- package/lib/browser/ai-getting-started-commands.d.ts +10 -0
- package/lib/browser/ai-getting-started-commands.d.ts.map +1 -0
- package/lib/browser/ai-getting-started-commands.js +34 -0
- package/lib/browser/ai-getting-started-commands.js.map +1 -0
- package/lib/browser/ai-getting-started-contribution.d.ts +20 -0
- package/lib/browser/ai-getting-started-contribution.d.ts.map +1 -0
- package/lib/browser/ai-getting-started-contribution.js +70 -0
- package/lib/browser/ai-getting-started-contribution.js.map +1 -0
- package/lib/browser/ai-getting-started-walkthrough.d.ts +41 -0
- package/lib/browser/ai-getting-started-walkthrough.d.ts.map +1 -0
- package/lib/browser/ai-getting-started-walkthrough.js +225 -0
- package/lib/browser/ai-getting-started-walkthrough.js.map +1 -0
- package/lib/browser/ai-getting-started-walkthrough.spec.d.ts +2 -0
- package/lib/browser/ai-getting-started-walkthrough.spec.d.ts.map +1 -0
- package/lib/browser/ai-getting-started-walkthrough.spec.js +124 -0
- package/lib/browser/ai-getting-started-walkthrough.spec.js.map +1 -0
- package/lib/browser/ai-walkthrough-context-keys.d.ts +55 -0
- package/lib/browser/ai-walkthrough-context-keys.d.ts.map +1 -0
- package/lib/browser/ai-walkthrough-context-keys.js +145 -0
- package/lib/browser/ai-walkthrough-context-keys.js.map +1 -0
- package/lib/browser/ai-walkthrough-context-keys.spec.d.ts +2 -0
- package/lib/browser/ai-walkthrough-context-keys.spec.d.ts.map +1 -0
- package/lib/browser/ai-walkthrough-context-keys.spec.js +166 -0
- package/lib/browser/ai-walkthrough-context-keys.spec.js.map +1 -0
- package/lib/browser/chat-session-item.js +2 -2
- package/lib/browser/chat-session-item.js.map +1 -1
- package/lib/browser/commit-message-agent.d.ts +31 -0
- package/lib/browser/commit-message-agent.d.ts.map +1 -0
- package/lib/browser/commit-message-agent.js +99 -0
- package/lib/browser/commit-message-agent.js.map +1 -0
- package/lib/browser/commit-message-agent.spec.d.ts +2 -0
- package/lib/browser/commit-message-agent.spec.d.ts.map +1 -0
- package/lib/browser/commit-message-agent.spec.js +82 -0
- package/lib/browser/commit-message-agent.spec.js.map +1 -0
- package/lib/browser/commit-message-command-contribution.d.ts +15 -0
- package/lib/browser/commit-message-command-contribution.d.ts.map +1 -0
- package/lib/browser/commit-message-command-contribution.js +73 -0
- package/lib/browser/commit-message-command-contribution.js.map +1 -0
- package/lib/browser/commit-message-commands.d.ts +5 -0
- package/lib/browser/commit-message-commands.d.ts.map +1 -0
- package/lib/browser/commit-message-commands.js +28 -0
- package/lib/browser/commit-message-commands.js.map +1 -0
- package/lib/browser/commit-message-prompt-template.d.ts +11 -0
- package/lib/browser/commit-message-prompt-template.d.ts.map +1 -0
- package/lib/browser/commit-message-prompt-template.js +63 -0
- package/lib/browser/commit-message-prompt-template.js.map +1 -0
- package/lib/browser/commit-message-runner.d.ts +41 -0
- package/lib/browser/commit-message-runner.d.ts.map +1 -0
- package/lib/browser/commit-message-runner.js +143 -0
- package/lib/browser/commit-message-runner.js.map +1 -0
- package/lib/browser/commit-message-runner.spec.d.ts +2 -0
- package/lib/browser/commit-message-runner.spec.d.ts.map +1 -0
- package/lib/browser/commit-message-runner.spec.js +206 -0
- package/lib/browser/commit-message-runner.spec.js.map +1 -0
- package/lib/browser/frontend-module.d.ts.map +1 -1
- package/lib/browser/frontend-module.js +25 -0
- package/lib/browser/frontend-module.js.map +1 -1
- package/lib/browser/git-changes-tool.d.ts +73 -0
- package/lib/browser/git-changes-tool.d.ts.map +1 -0
- package/lib/browser/git-changes-tool.js +217 -0
- package/lib/browser/git-changes-tool.js.map +1 -0
- package/lib/browser/git-changes-tool.spec.d.ts +2 -0
- package/lib/browser/git-changes-tool.spec.d.ts.map +1 -0
- package/lib/browser/git-changes-tool.spec.js +167 -0
- package/lib/browser/git-changes-tool.spec.js.map +1 -0
- package/lib/browser/ide-chat-welcome-message-provider.d.ts +26 -28
- package/lib/browser/ide-chat-welcome-message-provider.d.ts.map +1 -1
- package/lib/browser/ide-chat-welcome-message-provider.js +61 -151
- package/lib/browser/ide-chat-welcome-message-provider.js.map +1 -1
- package/lib/browser/review/pr-review-capability-contribution.js +1 -1
- package/lib/browser/review/pr-review-capability-contribution.js.map +1 -1
- package/lib/browser/user-interaction-tool-renderer.d.ts +6 -0
- package/lib/browser/user-interaction-tool-renderer.d.ts.map +1 -1
- package/lib/browser/user-interaction-tool-renderer.js +36 -7
- package/lib/browser/user-interaction-tool-renderer.js.map +1 -1
- package/lib/browser/user-interaction-tool.d.ts +23 -1
- package/lib/browser/user-interaction-tool.d.ts.map +1 -1
- package/lib/browser/user-interaction-tool.js +56 -17
- package/lib/browser/user-interaction-tool.js.map +1 -1
- package/lib/browser/user-interaction-tool.spec.js +151 -2
- package/lib/browser/user-interaction-tool.spec.js.map +1 -1
- package/lib/common/command-prompt-template.js +1 -1
- package/lib/common/user-interaction-tool.d.ts +19 -1
- package/lib/common/user-interaction-tool.d.ts.map +1 -1
- package/lib/common/user-interaction-tool.js +100 -29
- package/lib/common/user-interaction-tool.js.map +1 -1
- package/lib/common/user-interaction-tool.spec.js +215 -174
- package/lib/common/user-interaction-tool.spec.js.map +1 -1
- package/package.json +26 -25
- package/src/browser/ai-allow-all-mode-chat-banner.tsx +2 -2
- package/src/browser/ai-aware-scm-commit-widget.tsx +153 -0
- package/src/browser/ai-configuration/ai-configuration-detail-widget.spec.ts +14 -14
- package/src/browser/ai-configuration/ai-configuration-detail-widget.tsx +12 -7
- package/src/browser/ai-configuration/ai-configuration-tree-widget.tsx +8 -0
- package/src/browser/ai-configuration/categories/agent-capabilities-settings.tsx +7 -8
- package/src/browser/ai-configuration/categories/agent-chips.tsx +1 -1
- package/src/browser/ai-configuration/categories/model-aliases-configuration-category.spec.ts +40 -2
- package/src/browser/ai-configuration/categories/model-aliases-configuration-category.tsx +31 -10
- package/src/browser/ai-configuration/categories/models-configuration-category.spec.ts +47 -2
- package/src/browser/ai-configuration/categories/models-configuration-category.tsx +52 -5
- package/src/browser/ai-configuration/categories/provider-model-discovery-section.spec.tsx +451 -0
- package/src/browser/ai-configuration/categories/provider-model-discovery-section.tsx +341 -0
- package/src/browser/ai-configuration/language-model-renderer.tsx +11 -3
- package/src/browser/ai-getting-started-commands.ts +33 -0
- package/src/browser/ai-getting-started-contribution.ts +64 -0
- package/src/browser/ai-getting-started-walkthrough.spec.ts +146 -0
- package/src/browser/ai-getting-started-walkthrough.ts +249 -0
- package/src/browser/ai-walkthrough-context-keys.spec.ts +209 -0
- package/src/browser/ai-walkthrough-context-keys.ts +147 -0
- package/src/browser/chat-session-item.tsx +2 -2
- package/src/browser/commit-message-agent.spec.ts +97 -0
- package/src/browser/commit-message-agent.ts +113 -0
- package/src/browser/commit-message-command-contribution.ts +67 -0
- package/src/browser/commit-message-commands.ts +25 -0
- package/src/browser/commit-message-prompt-template.ts +65 -0
- package/src/browser/commit-message-runner.spec.ts +266 -0
- package/src/browser/commit-message-runner.ts +163 -0
- package/src/browser/frontend-module.ts +27 -0
- package/src/browser/git-changes-tool.spec.ts +200 -0
- package/src/browser/git-changes-tool.ts +239 -0
- package/src/browser/ide-chat-welcome-message-provider.tsx +93 -230
- package/src/browser/review/pr-review-capability-contribution.ts +1 -1
- package/src/browser/style/commit-message-overlay.css +64 -0
- package/src/browser/style/index.css +4 -42
- package/src/browser/user-interaction-tool-renderer.tsx +38 -8
- package/src/browser/user-interaction-tool.spec.ts +201 -2
- package/src/browser/user-interaction-tool.ts +69 -18
- package/src/common/command-prompt-template.ts +1 -1
- package/src/common/user-interaction-tool.spec.ts +217 -161
- 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
|
-
|
|
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
|
-
|
|
498
|
-
|
|
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
|
|
508
|
-
return
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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: '
|
|
107
|
-
+ 'a
|
|
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
|
|
320
|
-
//
|
|
321
|
-
const
|
|
322
|
-
if (!
|
|
323
|
-
return JSON.stringify({ error:
|
|
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
|
-
|
|
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);
|