@theia/ai-ide 1.76.0-next.7 → 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
|
@@ -15,205 +15,256 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import { expect } from 'chai';
|
|
18
|
-
import { buildDiffLabel, parseUserInteractionArgs, parseUserInteractionInput } from './user-interaction-tool';
|
|
18
|
+
import { UserInteractionArgs, buildDiffLabel, parseUserInteractionArgs, parseUserInteractionInput } from './user-interaction-tool';
|
|
19
|
+
|
|
20
|
+
function expectRejected(input: string | undefined): string {
|
|
21
|
+
const result = parseUserInteractionArgs(input);
|
|
22
|
+
expect(result.ok, `expected a rejection but got: ${JSON.stringify(result)}`).to.be.false;
|
|
23
|
+
return (result as { ok: false, error: string }).error;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function expectAccepted(input: string): UserInteractionArgs {
|
|
27
|
+
const result = parseUserInteractionArgs(input);
|
|
28
|
+
expect(result.ok, `expected acceptance but got: ${JSON.stringify(result)}`).to.be.true;
|
|
29
|
+
return (result as { ok: true, args: UserInteractionArgs }).args;
|
|
30
|
+
}
|
|
19
31
|
|
|
20
32
|
describe('parseUserInteractionArgs', () => {
|
|
21
|
-
it('should return undefined for undefined input', () => {
|
|
22
|
-
expect(parseUserInteractionArgs(undefined)).to.be.undefined;
|
|
23
|
-
});
|
|
24
33
|
|
|
25
|
-
|
|
26
|
-
expect(parseUserInteractionArgs('not json')).to.be.undefined;
|
|
27
|
-
});
|
|
34
|
+
describe('rejecting malformed arguments', () => {
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
});
|
|
36
|
+
it('should reject undefined input', () => {
|
|
37
|
+
expect(expectRejected(undefined)).to.match(/no arguments/i);
|
|
38
|
+
});
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
40
|
+
it('should reject input that is not valid JSON', () => {
|
|
41
|
+
expect(expectRejected('not json')).to.match(/valid JSON/i);
|
|
42
|
+
});
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
});
|
|
44
|
+
it('should reject arguments without an interactions property', () => {
|
|
45
|
+
expect(expectRejected(JSON.stringify({ foo: 'bar' }))).to.match(/"interactions" must be an array of step objects/);
|
|
46
|
+
});
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
interactions
|
|
47
|
-
|
|
48
|
-
{ title: 'No message' },
|
|
49
|
-
{ message: 'No title' }
|
|
50
|
-
]
|
|
48
|
+
it('should reject a JSON-encoded argument blob and name the received type', () => {
|
|
49
|
+
const error = expectRejected(JSON.stringify('{"interactions":[{"title":"T","message":"M"}]}'));
|
|
50
|
+
expect(error).to.match(/arguments must be an object with an "interactions" array, received string/i);
|
|
51
|
+
expect(error).to.match(/do not JSON-encode nested values/i);
|
|
51
52
|
});
|
|
52
|
-
const result = parseUserInteractionArgs(input);
|
|
53
|
-
expect(result).to.not.be.undefined;
|
|
54
|
-
expect(result!.interactions).to.have.length(1);
|
|
55
|
-
expect(result!.interactions[0].title).to.equal('Valid');
|
|
56
|
-
});
|
|
57
53
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
interactions: [{ title: 'Info', message: 'Just so you know' }]
|
|
54
|
+
it('should reject a top-level array', () => {
|
|
55
|
+
expect(expectRejected('[]')).to.match(/arguments must be an object with an "interactions" array, received array/i);
|
|
61
56
|
});
|
|
62
|
-
const result = parseUserInteractionArgs(input);
|
|
63
|
-
expect(result).to.not.be.undefined;
|
|
64
|
-
expect(result!.interactions[0].options).to.be.undefined;
|
|
65
|
-
});
|
|
66
57
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
interactions: [{
|
|
70
|
-
title: 'T', message: 'M',
|
|
71
|
-
options: [{ text: 'A', value: 'a' }, { bad: true }, { text: 'B', value: 'b' }]
|
|
72
|
-
}]
|
|
58
|
+
it('should reject top-level null', () => {
|
|
59
|
+
expect(expectRejected('null')).to.match(/arguments must be an object with an "interactions" array, received null/i);
|
|
73
60
|
});
|
|
74
|
-
const result = parseUserInteractionArgs(input);
|
|
75
|
-
expect(result!.interactions[0].options).to.have.length(2);
|
|
76
|
-
});
|
|
77
61
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
interactions
|
|
81
|
-
|
|
82
|
-
options: [{ bad: true }]
|
|
83
|
-
}]
|
|
62
|
+
it('should reject a JSON-encoded interactions string and name the received type', () => {
|
|
63
|
+
const error = expectRejected(JSON.stringify({ interactions: '[{"title":"T","message":"M"}]' }));
|
|
64
|
+
expect(error).to.match(/"interactions" must be an array of step objects, received string/);
|
|
65
|
+
expect(error).to.match(/do not JSON-encode nested values/i);
|
|
84
66
|
});
|
|
85
|
-
const result = parseUserInteractionArgs(input);
|
|
86
|
-
expect(result!.interactions[0].options).to.be.undefined;
|
|
87
|
-
});
|
|
88
67
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
68
|
+
it('should reject an empty interactions array', () => {
|
|
69
|
+
expect(expectRejected(JSON.stringify({ interactions: [] }))).to.match(/at least one step/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should reject a step that is missing its message, naming the step position', () => {
|
|
73
|
+
const error = expectRejected(JSON.stringify({
|
|
74
|
+
interactions: [{ title: 'Valid', message: 'Hello' }, { title: 'No message' }]
|
|
75
|
+
}));
|
|
76
|
+
expect(error).to.match(/step 2: "title" and "message" are required strings/i);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should reject a step that is missing its title, naming the step position', () => {
|
|
80
|
+
const error = expectRejected(JSON.stringify({
|
|
81
|
+
interactions: [{ message: 'No title' }]
|
|
82
|
+
}));
|
|
83
|
+
expect(error).to.match(/step 1: "title" and "message" are required strings/i);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should reject a step whose options are a JSON-encoded string', () => {
|
|
87
|
+
const error = expectRejected(JSON.stringify({
|
|
88
|
+
interactions: [{ title: 'Apply fix?', message: 'M', options: '[{"text":"Yes","value":"yes"}]' }]
|
|
89
|
+
}));
|
|
90
|
+
expect(error).to.match(/step 1: "options" must be an array of \{text, value\} objects, received string/i);
|
|
91
|
+
expect(error).to.match(/do not JSON-encode nested values/i);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should reject a step when a single option is malformed, even though others are valid', () => {
|
|
95
|
+
const error = expectRejected(JSON.stringify({
|
|
96
|
+
interactions: [{
|
|
97
|
+
title: 'T', message: 'M',
|
|
98
|
+
options: [{ text: 'A', value: 'a' }, { label: 'B', value: 'b' }, { text: 'C', value: 'c' }]
|
|
99
|
+
}]
|
|
100
|
+
}));
|
|
101
|
+
expect(error).to.match(/step 1, option 2: "text" and "value" are required strings/i);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should reject options given as an object rather than an array', () => {
|
|
105
|
+
const error = expectRejected(JSON.stringify({
|
|
106
|
+
interactions: [{ title: 'T', message: 'M', options: { text: 'Yes', value: 'yes' } }]
|
|
107
|
+
}));
|
|
108
|
+
expect(error).to.match(/step 1: "options" must be an array of \{text, value\} objects, received object/i);
|
|
96
109
|
});
|
|
97
|
-
const result = parseUserInteractionArgs(input);
|
|
98
|
-
expect(result!.interactions[0].links).to.deep.equal([{ ref: 'src/index.ts' }]);
|
|
99
|
-
});
|
|
100
110
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
it('should reject plain string options and name the received type', () => {
|
|
112
|
+
const error = expectRejected(JSON.stringify({
|
|
113
|
+
interactions: [{ title: 'T', message: 'M', options: ['Yes', 'No'] }]
|
|
114
|
+
}));
|
|
115
|
+
expect(error).to.match(/step 1, option 1: "text" and "value" are required strings, received string/i);
|
|
116
|
+
expect(error).to.match(/do not JSON-encode nested values/i);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should reject a JSON-encoded links string', () => {
|
|
120
|
+
const error = expectRejected(JSON.stringify({
|
|
121
|
+
interactions: [{ title: 'T', message: 'M', links: '[{"ref":"a.ts"}]' }]
|
|
122
|
+
}));
|
|
123
|
+
expect(error).to.match(/step 1: "links" must be an array of link objects, received string/i);
|
|
124
|
+
expect(error).to.match(/do not JSON-encode nested values/i);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should report the offending option position within the offending step', () => {
|
|
128
|
+
const error = expectRejected(JSON.stringify({
|
|
129
|
+
interactions: [
|
|
130
|
+
{ title: 'Fine', message: 'M', options: [{ text: 'A', value: 'a' }] },
|
|
131
|
+
{ title: 'Broken', message: 'M', options: [{ text: 'A', value: 'a' }, { text: 'B' }] }
|
|
109
132
|
]
|
|
110
|
-
}
|
|
133
|
+
}));
|
|
134
|
+
expect(error).to.match(/step 2, option 2: "text" and "value" are required strings/i);
|
|
111
135
|
});
|
|
112
|
-
const result = parseUserInteractionArgs(input);
|
|
113
|
-
expect(result!.interactions[0].links).to.have.length(2);
|
|
114
136
|
});
|
|
115
137
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
138
|
+
describe('accepting well-formed arguments', () => {
|
|
139
|
+
|
|
140
|
+
it('should accept a step without options (informational)', () => {
|
|
141
|
+
const args = expectAccepted(JSON.stringify({
|
|
142
|
+
interactions: [{ title: 'Info', message: 'Just so you know' }]
|
|
143
|
+
}));
|
|
144
|
+
expect(args.interactions[0].options).to.be.undefined;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should accept an explicitly empty options array as informational', () => {
|
|
148
|
+
const args = expectAccepted(JSON.stringify({
|
|
149
|
+
interactions: [{ title: 'Info', message: 'Just so you know', options: [] }]
|
|
150
|
+
}));
|
|
151
|
+
expect(args.interactions[0].options).to.be.undefined;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should accept multiple steps in order', () => {
|
|
155
|
+
const args = expectAccepted(JSON.stringify({
|
|
156
|
+
interactions: [
|
|
157
|
+
{ title: 'Overview', message: 'PR summary' },
|
|
158
|
+
{ title: 'Area 1', message: 'finding', options: [{ text: 'OK', value: 'approve' }] },
|
|
159
|
+
{ title: 'Area 2', message: 'no findings' }
|
|
125
160
|
]
|
|
126
|
-
}
|
|
161
|
+
}));
|
|
162
|
+
expect(args.interactions).to.have.length(3);
|
|
163
|
+
expect(args.interactions[1].options).to.have.length(1);
|
|
164
|
+
expect(args.interactions[2].options).to.be.undefined;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should preserve buttonLabel and description in options', () => {
|
|
168
|
+
const args = expectAccepted(JSON.stringify({
|
|
169
|
+
interactions: [{
|
|
170
|
+
title: 'T', message: 'M',
|
|
171
|
+
options: [{ text: 'Confirm changes', value: 'confirm', description: 'Applies the patch', buttonLabel: '✅ Confirm' }]
|
|
172
|
+
}]
|
|
173
|
+
}));
|
|
174
|
+
expect(args.interactions[0].options![0].buttonLabel).to.equal('✅ Confirm');
|
|
175
|
+
expect(args.interactions[0].options![0].description).to.equal('Applies the patch');
|
|
127
176
|
});
|
|
128
|
-
const result = parseUserInteractionArgs(input);
|
|
129
|
-
expect(result!.interactions[0].links).to.have.length(1);
|
|
130
177
|
});
|
|
131
178
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const input = JSON.stringify({
|
|
141
|
-
interactions: ['README.md', 'package.json', 'CONTRIBUTING.md'].map((path, index) => ({
|
|
142
|
-
title: `File link: ${path}`,
|
|
143
|
-
message: 'Open the file link.',
|
|
144
|
-
links: [{
|
|
145
|
-
ref: { path, gitRef: '', line: 1, empty: false, label: '' },
|
|
146
|
-
rightRef: emptyRightRefPlaceholder,
|
|
147
|
-
label: `Open ${path}`,
|
|
148
|
-
autoOpen: index === 0
|
|
179
|
+
describe('links remain leniently filtered', () => {
|
|
180
|
+
|
|
181
|
+
it('should normalize a singular link into a links array on a step', () => {
|
|
182
|
+
const args = expectAccepted(JSON.stringify({
|
|
183
|
+
interactions: [{
|
|
184
|
+
title: 'T', message: 'M',
|
|
185
|
+
options: [{ text: 'A', value: 'a' }],
|
|
186
|
+
link: { ref: 'src/index.ts' }
|
|
149
187
|
}]
|
|
150
|
-
}))
|
|
188
|
+
}));
|
|
189
|
+
expect(args.interactions[0].links).to.deep.equal([{ ref: 'src/index.ts' }]);
|
|
151
190
|
});
|
|
152
191
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
192
|
+
it('should accept a links array with multiple entries', () => {
|
|
193
|
+
const args = expectAccepted(JSON.stringify({
|
|
194
|
+
interactions: [{
|
|
195
|
+
title: 'T', message: 'M',
|
|
196
|
+
links: [{ ref: 'a.ts' }, { ref: 'b.ts', label: 'B' }]
|
|
197
|
+
}]
|
|
198
|
+
}));
|
|
199
|
+
expect(args.interactions[0].links).to.have.length(2);
|
|
200
|
+
});
|
|
160
201
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
202
|
+
it('should drop invalid links from a step without rejecting the step', () => {
|
|
203
|
+
const args = expectAccepted(JSON.stringify({
|
|
204
|
+
interactions: [{
|
|
205
|
+
title: 'T', message: 'M',
|
|
206
|
+
links: [{ ref: 'a.ts' }, { nope: true }, { ref: 42 }]
|
|
207
|
+
}]
|
|
208
|
+
}));
|
|
209
|
+
expect(args.interactions[0].links).to.have.length(1);
|
|
168
210
|
});
|
|
169
|
-
const result = parseUserInteractionArgs(input);
|
|
170
|
-
expect(result!.interactions).to.have.length(3);
|
|
171
|
-
expect(result!.interactions[1].options).to.have.length(1);
|
|
172
|
-
expect(result!.interactions[2].options).to.be.undefined;
|
|
173
|
-
});
|
|
174
211
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
212
|
+
it('should ignore invalid rightRef placeholders and keep multi-step file links', () => {
|
|
213
|
+
const emptyRightRefPlaceholder = {
|
|
214
|
+
path: '',
|
|
215
|
+
gitRef: '',
|
|
216
|
+
line: 0,
|
|
217
|
+
empty: false,
|
|
218
|
+
label: ''
|
|
219
|
+
};
|
|
220
|
+
const args = expectAccepted(JSON.stringify({
|
|
221
|
+
interactions: ['README.md', 'package.json', 'CONTRIBUTING.md'].map((path, index) => ({
|
|
222
|
+
title: `File link: ${path}`,
|
|
223
|
+
message: 'Open the file link.',
|
|
224
|
+
links: [{
|
|
225
|
+
ref: { path, gitRef: '', line: 1, empty: false, label: '' },
|
|
226
|
+
rightRef: emptyRightRefPlaceholder,
|
|
227
|
+
label: `Open ${path}`,
|
|
228
|
+
autoOpen: index === 0
|
|
229
|
+
}]
|
|
230
|
+
}))
|
|
231
|
+
}));
|
|
232
|
+
expect(args.interactions.map(step => step.links![0])).to.deep.equal([
|
|
233
|
+
{ ref: { path: 'README.md', line: 1 }, label: 'Open README.md', autoOpen: true },
|
|
234
|
+
{ ref: { path: 'package.json', line: 1 }, label: 'Open package.json', autoOpen: false },
|
|
235
|
+
{ ref: { path: 'CONTRIBUTING.md', line: 1 }, label: 'Open CONTRIBUTING.md', autoOpen: false }
|
|
236
|
+
]);
|
|
181
237
|
});
|
|
182
|
-
const result = parseUserInteractionArgs(input);
|
|
183
|
-
expect(result!.interactions[0].options![0].buttonLabel).to.equal('✅ Confirm');
|
|
184
|
-
});
|
|
185
238
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
239
|
+
it('should drop a link whose object ref has an empty path', () => {
|
|
240
|
+
const args = expectAccepted(JSON.stringify({
|
|
241
|
+
interactions: [{
|
|
242
|
+
title: 'T', message: 'M',
|
|
243
|
+
links: [{ ref: { path: '' } }]
|
|
244
|
+
}]
|
|
245
|
+
}));
|
|
246
|
+
expect(args.interactions[0].links).to.be.undefined;
|
|
192
247
|
});
|
|
193
|
-
const result = parseUserInteractionArgs(input);
|
|
194
|
-
expect(result!.interactions[0].links).to.be.undefined;
|
|
195
|
-
});
|
|
196
248
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
249
|
+
it('should accept step links with EmptyContentRef', () => {
|
|
250
|
+
const args = expectAccepted(JSON.stringify({
|
|
251
|
+
interactions: [{
|
|
252
|
+
title: 'T', message: 'M',
|
|
253
|
+
links: [{ ref: { empty: true, label: 'New file' } }]
|
|
254
|
+
}]
|
|
255
|
+
}));
|
|
256
|
+
expect(args.interactions[0].links![0].ref).to.deep.equal({ empty: true, label: 'New file' });
|
|
203
257
|
});
|
|
204
|
-
const result = parseUserInteractionArgs(input);
|
|
205
|
-
expect(result!.interactions[0].links![0].ref).to.deep.equal({ empty: true, label: 'New file' });
|
|
206
|
-
});
|
|
207
258
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
259
|
+
it('should accept step links with EmptyContentRef as rightRef', () => {
|
|
260
|
+
const args = expectAccepted(JSON.stringify({
|
|
261
|
+
interactions: [{
|
|
262
|
+
title: 'T', message: 'M',
|
|
263
|
+
links: [{ ref: 'src/old.ts', rightRef: { empty: true } }]
|
|
264
|
+
}]
|
|
265
|
+
}));
|
|
266
|
+
expect(args.interactions[0].links![0].rightRef).to.deep.equal({ empty: true });
|
|
214
267
|
});
|
|
215
|
-
const result = parseUserInteractionArgs(input);
|
|
216
|
-
expect(result!.interactions[0].links![0].rightRef).to.deep.equal({ empty: true });
|
|
217
268
|
});
|
|
218
269
|
});
|
|
219
270
|
|
|
@@ -241,6 +292,11 @@ describe('parseUserInteractionInput', () => {
|
|
|
241
292
|
expect(parseUserInteractionInput(input).title).to.equal('Streaming Title');
|
|
242
293
|
});
|
|
243
294
|
|
|
295
|
+
it('should extract a partial title from JSON truncated mid-title', () => {
|
|
296
|
+
const input = '{"interactions":[{"title":"Streaming ti';
|
|
297
|
+
expect(parseUserInteractionInput(input)).to.deep.equal({ title: 'Streaming ti', stepCount: 0 });
|
|
298
|
+
});
|
|
299
|
+
|
|
244
300
|
it('should return empty title from incomplete JSON without title field', () => {
|
|
245
301
|
const input = '{"interactions": [{"message": "no title here';
|
|
246
302
|
expect(parseUserInteractionInput(input).title).to.equal('');
|
|
@@ -155,50 +155,104 @@ export function parseUserInteractionResult(raw: unknown): UserInteractionResult
|
|
|
155
155
|
return obj as unknown as UserInteractionResult;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Outcome of validating the raw `userInteraction` tool arguments.
|
|
160
|
+
*
|
|
161
|
+
* Validation is strict for anything that determines whether — and how — the user is asked
|
|
162
|
+
* (`interactions`, each step, and each option), because silently discarding those turns a
|
|
163
|
+
* decision step into an auto-completed informational one and skips the confirmation gate.
|
|
164
|
+
* Individual `links` stay lenient, because they are decoration: an unresolvable link should
|
|
165
|
+
* not discard the surrounding findings. The `links` container itself is still strict, since a
|
|
166
|
+
* JSON-encoded list drops every link at once and leaves the step without the content it is
|
|
167
|
+
* about — the same failure as a JSON-encoded `options` list, not an unresolvable entry.
|
|
168
|
+
*/
|
|
169
|
+
export type UserInteractionValidation =
|
|
170
|
+
| { ok: true, args: UserInteractionArgs }
|
|
171
|
+
| { ok: false, error: string };
|
|
172
|
+
|
|
173
|
+
type StepValidation =
|
|
174
|
+
| { ok: true, step: UserInteractionStep }
|
|
175
|
+
| { ok: false, error: string };
|
|
176
|
+
|
|
177
|
+
type OptionsValidation =
|
|
178
|
+
| { ok: true, options?: UserInteractionOption[] }
|
|
179
|
+
| { ok: false, error: string };
|
|
180
|
+
|
|
181
|
+
const JSON_ENCODING_HINT = 'Do not JSON-encode nested values - pass real arrays and objects.';
|
|
182
|
+
|
|
183
|
+
function describeType(value: unknown): string {
|
|
184
|
+
// eslint-disable-next-line no-null/no-null
|
|
185
|
+
if (value === null) {
|
|
186
|
+
return 'null';
|
|
187
|
+
}
|
|
188
|
+
if (Array.isArray(value)) {
|
|
189
|
+
return 'array';
|
|
190
|
+
}
|
|
191
|
+
return typeof value;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Describe the received type, adding the JSON-encoding hint when a string was received where
|
|
196
|
+
* a structured value was expected - by far the most common way agents get these arguments wrong.
|
|
197
|
+
*/
|
|
198
|
+
function describeReceived(value: unknown): string {
|
|
199
|
+
const type = describeType(value);
|
|
200
|
+
return type === 'string' ? `${type}. ${JSON_ENCODING_HINT}` : `${type}.`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function parseUserInteractionArgs(args: string | undefined): UserInteractionValidation {
|
|
159
204
|
if (!args) {
|
|
160
|
-
return
|
|
205
|
+
return { ok: false, error: 'No arguments were provided.' };
|
|
161
206
|
}
|
|
207
|
+
let parsed: unknown;
|
|
162
208
|
try {
|
|
163
|
-
|
|
164
|
-
if (!Array.isArray(parsed.interactions)) {
|
|
165
|
-
return undefined;
|
|
166
|
-
}
|
|
167
|
-
const validSteps = parsed.interactions
|
|
168
|
-
.map(parseStep)
|
|
169
|
-
.filter((step: UserInteractionStep | undefined): step is UserInteractionStep => step !== undefined);
|
|
170
|
-
if (validSteps.length === 0) {
|
|
171
|
-
return undefined;
|
|
172
|
-
}
|
|
173
|
-
return { interactions: validSteps };
|
|
209
|
+
parsed = JSON.parse(args);
|
|
174
210
|
} catch {
|
|
175
|
-
return
|
|
211
|
+
return { ok: false, error: 'Arguments are not valid JSON.' };
|
|
176
212
|
}
|
|
213
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
214
|
+
return { ok: false, error: `Arguments must be an object with an "interactions" array, received ${describeReceived(parsed)}` };
|
|
215
|
+
}
|
|
216
|
+
const interactions = (parsed as Record<string, unknown>).interactions;
|
|
217
|
+
if (!Array.isArray(interactions)) {
|
|
218
|
+
return { ok: false, error: `"interactions" must be an array of step objects, received ${describeReceived(interactions)}` };
|
|
219
|
+
}
|
|
220
|
+
if (interactions.length === 0) {
|
|
221
|
+
return { ok: false, error: '"interactions" must contain at least one step.' };
|
|
222
|
+
}
|
|
223
|
+
const steps: UserInteractionStep[] = [];
|
|
224
|
+
for (let index = 0; index < interactions.length; index++) {
|
|
225
|
+
const step = parseStep(interactions[index], index + 1);
|
|
226
|
+
if (!step.ok) {
|
|
227
|
+
return step;
|
|
228
|
+
}
|
|
229
|
+
steps.push(step.step);
|
|
230
|
+
}
|
|
231
|
+
return { ok: true, args: { interactions: steps } };
|
|
177
232
|
}
|
|
178
233
|
|
|
179
|
-
function parseStep(raw: unknown):
|
|
180
|
-
if (!raw || typeof raw !== 'object') {
|
|
181
|
-
return
|
|
234
|
+
function parseStep(raw: unknown, position: number): StepValidation {
|
|
235
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
236
|
+
return { ok: false, error: `Step ${position}: expected a step object, received ${describeReceived(raw)}` };
|
|
182
237
|
}
|
|
183
238
|
const obj = raw as Record<string, unknown>;
|
|
184
239
|
if (typeof obj.title !== 'string' || typeof obj.message !== 'string') {
|
|
185
|
-
return
|
|
240
|
+
return { ok: false, error: `Step ${position}: "title" and "message" are required strings.` };
|
|
186
241
|
}
|
|
187
|
-
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
(opt: unknown) => !!opt && typeof opt === 'object'
|
|
191
|
-
&& typeof (opt as Record<string, unknown>).text === 'string'
|
|
192
|
-
&& typeof (opt as Record<string, unknown>).value === 'string'
|
|
193
|
-
) as UserInteractionOption[];
|
|
194
|
-
options = validOptions.length > 0 ? validOptions : undefined;
|
|
242
|
+
const options = parseOptions(obj.options, position);
|
|
243
|
+
if (!options.ok) {
|
|
244
|
+
return options;
|
|
195
245
|
}
|
|
196
246
|
let links: UserInteractionLink[] | undefined;
|
|
197
|
-
|
|
198
|
-
|
|
247
|
+
// eslint-disable-next-line no-null/no-null
|
|
248
|
+
const rawLinks = obj.links === null ? undefined : obj.links;
|
|
249
|
+
if (Array.isArray(rawLinks)) {
|
|
250
|
+
const filtered = rawLinks
|
|
199
251
|
.map(normalizeUserInteractionLink)
|
|
200
252
|
.filter((link: UserInteractionLink | undefined): link is UserInteractionLink => link !== undefined);
|
|
201
253
|
links = filtered.length > 0 ? filtered : undefined;
|
|
254
|
+
} else if (rawLinks !== undefined) {
|
|
255
|
+
return { ok: false, error: `Step ${position}: "links" must be an array of link objects, received ${describeReceived(rawLinks)}` };
|
|
202
256
|
} else {
|
|
203
257
|
const link = normalizeUserInteractionLink(obj.link);
|
|
204
258
|
if (link) {
|
|
@@ -206,13 +260,54 @@ function parseStep(raw: unknown): UserInteractionStep | undefined {
|
|
|
206
260
|
}
|
|
207
261
|
}
|
|
208
262
|
return {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
263
|
+
ok: true,
|
|
264
|
+
step: {
|
|
265
|
+
title: obj.title,
|
|
266
|
+
message: obj.message,
|
|
267
|
+
options: options.options,
|
|
268
|
+
links
|
|
269
|
+
}
|
|
213
270
|
};
|
|
214
271
|
}
|
|
215
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Validate a step's `options`. An absent or explicitly empty list means the step is
|
|
275
|
+
* informational; anything else must be a fully well-formed array, since a partially
|
|
276
|
+
* dropped option would silently offer the user a different choice than the agent intended.
|
|
277
|
+
*/
|
|
278
|
+
function parseOptions(raw: unknown, stepPosition: number): OptionsValidation {
|
|
279
|
+
// eslint-disable-next-line no-null/no-null
|
|
280
|
+
if (raw === undefined || raw === null) {
|
|
281
|
+
return { ok: true, options: undefined };
|
|
282
|
+
}
|
|
283
|
+
if (!Array.isArray(raw)) {
|
|
284
|
+
return {
|
|
285
|
+
ok: false,
|
|
286
|
+
error: `Step ${stepPosition}: "options" must be an array of {text, value} objects, received ${describeReceived(raw)}`
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const options: UserInteractionOption[] = [];
|
|
290
|
+
for (let index = 0; index < raw.length; index++) {
|
|
291
|
+
const candidate = raw[index];
|
|
292
|
+
const obj = candidate as Record<string, unknown> | undefined;
|
|
293
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj) || typeof obj.text !== 'string' || typeof obj.value !== 'string') {
|
|
294
|
+
return {
|
|
295
|
+
ok: false,
|
|
296
|
+
error: `Step ${stepPosition}, option ${index + 1}: "text" and "value" are required strings, received ${describeReceived(candidate)}`
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const option: UserInteractionOption = { text: obj.text, value: obj.value };
|
|
300
|
+
if (typeof obj.description === 'string') {
|
|
301
|
+
option.description = obj.description;
|
|
302
|
+
}
|
|
303
|
+
if (typeof obj.buttonLabel === 'string') {
|
|
304
|
+
option.buttonLabel = obj.buttonLabel;
|
|
305
|
+
}
|
|
306
|
+
options.push(option);
|
|
307
|
+
}
|
|
308
|
+
return { ok: true, options: options.length > 0 ? options : undefined };
|
|
309
|
+
}
|
|
310
|
+
|
|
216
311
|
function normalizeContentRef(ref: unknown): ContentRef | undefined {
|
|
217
312
|
if (typeof ref === 'string') {
|
|
218
313
|
return ref.length > 0 ? ref : undefined;
|