@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.
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
@@ -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
- it('should return undefined for invalid JSON', () => {
26
- expect(parseUserInteractionArgs('not json')).to.be.undefined;
27
- });
34
+ describe('rejecting malformed arguments', () => {
28
35
 
29
- it('should return undefined when interactions is missing', () => {
30
- const input = JSON.stringify({ foo: 'bar' });
31
- expect(parseUserInteractionArgs(input)).to.be.undefined;
32
- });
36
+ it('should reject undefined input', () => {
37
+ expect(expectRejected(undefined)).to.match(/no arguments/i);
38
+ });
33
39
 
34
- it('should return undefined when interactions is not an array', () => {
35
- const input = JSON.stringify({ interactions: 'nope' });
36
- expect(parseUserInteractionArgs(input)).to.be.undefined;
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
- it('should return undefined when no step is valid', () => {
40
- const input = JSON.stringify({ interactions: [{ foo: 'bar' }, { title: 1 }] });
41
- expect(parseUserInteractionArgs(input)).to.be.undefined;
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
- it('should drop steps that are missing title or message', () => {
45
- const input = JSON.stringify({
46
- interactions: [
47
- { title: 'Valid', message: 'Hello' },
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
- it('should accept a step without options (informational)', () => {
59
- const input = JSON.stringify({
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
- it('should filter out invalid options within a step', () => {
68
- const input = JSON.stringify({
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
- it('should drop options array if no options are valid', () => {
79
- const input = JSON.stringify({
80
- interactions: [{
81
- title: 'T', message: 'M',
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
- it('should normalize singular link into a links array on a step', () => {
90
- const input = JSON.stringify({
91
- interactions: [{
92
- title: 'T', message: 'M',
93
- options: [{ text: 'A', value: 'a' }],
94
- link: { ref: 'src/index.ts' }
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
- it('should accept a links array with multiple entries', () => {
102
- const input = JSON.stringify({
103
- interactions: [{
104
- title: 'T', message: 'M',
105
- options: [{ text: 'A', value: 'a' }],
106
- links: [
107
- { ref: 'src/a.ts' },
108
- { ref: 'src/old.ts', rightRef: 'src/new.ts' }
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
- it('should filter out invalid links from a step', () => {
117
- const input = JSON.stringify({
118
- interactions: [{
119
- title: 'T', message: 'M',
120
- options: [{ text: 'A', value: 'a' }],
121
- links: [
122
- { ref: 'src/a.ts' },
123
- { noRef: true },
124
- { ref: '' }
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
- it('should ignore invalid rightRef placeholders and keep multi-step file links', () => {
133
- const emptyRightRefPlaceholder = {
134
- path: '',
135
- gitRef: '',
136
- line: 0,
137
- empty: false,
138
- label: ''
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
- const result = parseUserInteractionArgs(input);
154
- expect(result!.interactions.map(step => step.links![0])).to.deep.equal([
155
- { ref: { path: 'README.md', line: 1 }, label: 'Open README.md', autoOpen: true },
156
- { ref: { path: 'package.json', line: 1 }, label: 'Open package.json', autoOpen: false },
157
- { ref: { path: 'CONTRIBUTING.md', line: 1 }, label: 'Open CONTRIBUTING.md', autoOpen: false }
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
- it('should accept multiple steps in order', () => {
162
- const input = JSON.stringify({
163
- interactions: [
164
- { title: 'Overview', message: 'PR summary' },
165
- { title: 'Area 1', message: 'finding', options: [{ text: 'OK', value: 'approve' }] },
166
- { title: 'Area 2', message: 'no findings' }
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
- it('should preserve buttonLabel in options', () => {
176
- const input = JSON.stringify({
177
- interactions: [{
178
- title: 'T', message: 'M',
179
- options: [{ text: 'Confirm changes', value: 'confirm', buttonLabel: '✅ Confirm' }]
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
- it('should reject step links with empty path in object ref', () => {
187
- const input = JSON.stringify({
188
- interactions: [{
189
- title: 'T', message: 'M',
190
- links: [{ ref: { path: '' } }]
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
- it('should accept step links with EmptyContentRef', () => {
198
- const input = JSON.stringify({
199
- interactions: [{
200
- title: 'T', message: 'M',
201
- links: [{ ref: { empty: true, label: 'New file' } }]
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
- it('should accept step links with EmptyContentRef as rightRef', () => {
209
- const input = JSON.stringify({
210
- interactions: [{
211
- title: 'T', message: 'M',
212
- links: [{ ref: 'src/old.ts', rightRef: { empty: true } }]
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
- export function parseUserInteractionArgs(args: string | undefined): UserInteractionArgs | undefined {
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 undefined;
205
+ return { ok: false, error: 'No arguments were provided.' };
161
206
  }
207
+ let parsed: unknown;
162
208
  try {
163
- const parsed = JSON.parse(args);
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 undefined;
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): UserInteractionStep | undefined {
180
- if (!raw || typeof raw !== 'object') {
181
- return undefined;
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 undefined;
240
+ return { ok: false, error: `Step ${position}: "title" and "message" are required strings.` };
186
241
  }
187
- let options: UserInteractionOption[] | undefined;
188
- if (Array.isArray(obj.options)) {
189
- const validOptions = obj.options.filter(
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
- if (Array.isArray(obj.links)) {
198
- const filtered = obj.links
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
- title: obj.title,
210
- message: obj.message,
211
- options,
212
- links
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;