@theia/ai-ide 1.71.0-next.8 → 1.71.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 (212) hide show
  1. package/lib/browser/agent-mode-confirmation-service.d.ts.map +1 -1
  2. package/lib/browser/agent-mode-confirmation-service.js +15 -15
  3. package/lib/browser/agent-mode-confirmation-service.js.map +1 -1
  4. package/lib/browser/ai-configuration/agent-configuration-widget.js +2 -2
  5. package/lib/browser/ai-configuration/agent-configuration-widget.js.map +1 -1
  6. package/lib/browser/ai-configuration/ai-configuration-view-contribution.js +1 -1
  7. package/lib/browser/ai-configuration/ai-configuration-view-contribution.js.map +1 -1
  8. package/lib/browser/ai-configuration/prompt-fragments-configuration-widget.d.ts.map +1 -1
  9. package/lib/browser/ai-configuration/prompt-fragments-configuration-widget.js +2 -1
  10. package/lib/browser/ai-configuration/prompt-fragments-configuration-widget.js.map +1 -1
  11. package/lib/browser/ai-configuration/skills-configuration-widget.d.ts.map +1 -1
  12. package/lib/browser/ai-configuration/skills-configuration-widget.js +2 -1
  13. package/lib/browser/ai-configuration/skills-configuration-widget.js.map +1 -1
  14. package/lib/browser/ai-ide-activation-service.d.ts +10 -1
  15. package/lib/browser/ai-ide-activation-service.d.ts.map +1 -1
  16. package/lib/browser/ai-ide-activation-service.js +41 -1
  17. package/lib/browser/ai-ide-activation-service.js.map +1 -1
  18. package/lib/browser/ai-workspace-restriction-contribution.d.ts +7 -0
  19. package/lib/browser/ai-workspace-restriction-contribution.d.ts.map +1 -0
  20. package/lib/browser/ai-workspace-restriction-contribution.js +42 -0
  21. package/lib/browser/ai-workspace-restriction-contribution.js.map +1 -0
  22. package/lib/browser/app-tester-prompt-template.js +1 -1
  23. package/lib/browser/architect-agent.d.ts +1 -0
  24. package/lib/browser/architect-agent.d.ts.map +1 -1
  25. package/lib/browser/architect-agent.js +5 -3
  26. package/lib/browser/architect-agent.js.map +1 -1
  27. package/lib/browser/architect-prompt-template.js +3 -3
  28. package/lib/browser/chat-session-card-action-contribution.js +1 -1
  29. package/lib/browser/chat-session-card-action-contribution.js.map +1 -1
  30. package/lib/browser/chat-sessions-welcome-message-provider.d.ts +1 -0
  31. package/lib/browser/chat-sessions-welcome-message-provider.d.ts.map +1 -1
  32. package/lib/browser/chat-sessions-welcome-message-provider.js +7 -1
  33. package/lib/browser/chat-sessions-welcome-message-provider.js.map +1 -1
  34. package/lib/browser/code-reviewer-agent.d.ts +1 -0
  35. package/lib/browser/code-reviewer-agent.d.ts.map +1 -1
  36. package/lib/browser/code-reviewer-agent.js +1 -0
  37. package/lib/browser/code-reviewer-agent.js.map +1 -1
  38. package/lib/browser/coder-agent.d.ts +4 -0
  39. package/lib/browser/coder-agent.d.ts.map +1 -1
  40. package/lib/browser/coder-agent.js +28 -4
  41. package/lib/browser/coder-agent.js.map +1 -1
  42. package/lib/browser/create-skill-agent.d.ts +1 -0
  43. package/lib/browser/create-skill-agent.d.ts.map +1 -1
  44. package/lib/browser/create-skill-agent.js +1 -0
  45. package/lib/browser/create-skill-agent.js.map +1 -1
  46. package/lib/browser/explore-agent.d.ts +1 -0
  47. package/lib/browser/explore-agent.d.ts.map +1 -1
  48. package/lib/browser/explore-agent.js +1 -0
  49. package/lib/browser/explore-agent.js.map +1 -1
  50. package/lib/browser/file-changeset-functions.d.ts.map +1 -1
  51. package/lib/browser/file-changeset-functions.js +17 -9
  52. package/lib/browser/file-changeset-functions.js.map +1 -1
  53. package/lib/browser/frontend-module.d.ts.map +1 -1
  54. package/lib/browser/frontend-module.js +12 -10
  55. package/lib/browser/frontend-module.js.map +1 -1
  56. package/lib/browser/github-capability-contribution.js +1 -1
  57. package/lib/browser/github-capability-contribution.js.map +1 -1
  58. package/lib/browser/github-prompt-template.js +1 -1
  59. package/lib/browser/ide-chat-welcome-message-provider.d.ts +4 -0
  60. package/lib/browser/ide-chat-welcome-message-provider.d.ts.map +1 -1
  61. package/lib/browser/ide-chat-welcome-message-provider.js +34 -0
  62. package/lib/browser/ide-chat-welcome-message-provider.js.map +1 -1
  63. package/lib/browser/project-info-agent.d.ts +1 -0
  64. package/lib/browser/project-info-agent.d.ts.map +1 -1
  65. package/lib/browser/project-info-agent.js +1 -0
  66. package/lib/browser/project-info-agent.js.map +1 -1
  67. package/lib/browser/{junior-agent.d.ts → review/pr-review-agent.d.ts} +7 -5
  68. package/lib/browser/review/pr-review-agent.d.ts.map +1 -0
  69. package/lib/browser/{junior-agent.js → review/pr-review-agent.js} +17 -15
  70. package/lib/browser/review/pr-review-agent.js.map +1 -0
  71. package/lib/browser/review/pr-review-prompt-template.d.ts +4 -0
  72. package/lib/browser/review/pr-review-prompt-template.d.ts.map +1 -0
  73. package/lib/browser/review/pr-review-prompt-template.js +437 -0
  74. package/lib/browser/review/pr-review-prompt-template.js.map +1 -0
  75. package/lib/browser/template-preference-contribution.d.ts +2 -0
  76. package/lib/browser/template-preference-contribution.d.ts.map +1 -1
  77. package/lib/browser/template-preference-contribution.js +43 -14
  78. package/lib/browser/template-preference-contribution.js.map +1 -1
  79. package/lib/browser/todo-tool-renderer.d.ts +1 -1
  80. package/lib/browser/todo-tool-renderer.d.ts.map +1 -1
  81. package/lib/browser/todo-tool-renderer.js +1 -1
  82. package/lib/browser/todo-tool-renderer.js.map +1 -1
  83. package/lib/browser/todo-tool.d.ts +0 -1
  84. package/lib/browser/todo-tool.d.ts.map +1 -1
  85. package/lib/browser/todo-tool.js +36 -16
  86. package/lib/browser/todo-tool.js.map +1 -1
  87. package/lib/browser/todo-tool.spec.d.ts +2 -0
  88. package/lib/browser/todo-tool.spec.d.ts.map +1 -0
  89. package/lib/browser/todo-tool.spec.js +44 -0
  90. package/lib/browser/todo-tool.spec.js.map +1 -0
  91. package/lib/browser/user-interaction-tool-renderer.d.ts +18 -0
  92. package/lib/browser/user-interaction-tool-renderer.d.ts.map +1 -0
  93. package/lib/browser/user-interaction-tool-renderer.js +330 -0
  94. package/lib/browser/user-interaction-tool-renderer.js.map +1 -0
  95. package/lib/browser/user-interaction-tool.d.ts +47 -0
  96. package/lib/browser/user-interaction-tool.d.ts.map +1 -0
  97. package/lib/browser/user-interaction-tool.js +397 -0
  98. package/lib/browser/user-interaction-tool.js.map +1 -0
  99. package/lib/browser/user-interaction-tool.spec.d.ts +2 -0
  100. package/lib/browser/user-interaction-tool.spec.d.ts.map +1 -0
  101. package/lib/browser/user-interaction-tool.spec.js +336 -0
  102. package/lib/browser/user-interaction-tool.spec.js.map +1 -0
  103. package/lib/browser/workspace-functions.d.ts.map +1 -1
  104. package/lib/browser/workspace-functions.js +9 -2
  105. package/lib/browser/workspace-functions.js.map +1 -1
  106. package/lib/browser/workspace-launch-provider.d.ts.map +1 -1
  107. package/lib/browser/workspace-launch-provider.js +9 -4
  108. package/lib/browser/workspace-launch-provider.js.map +1 -1
  109. package/lib/browser/workspace-launch-provider.spec.js +4 -4
  110. package/lib/browser/workspace-launch-provider.spec.js.map +1 -1
  111. package/lib/browser/workspace-task-provider.d.ts.map +1 -1
  112. package/lib/browser/workspace-task-provider.js +4 -1
  113. package/lib/browser/workspace-task-provider.js.map +1 -1
  114. package/lib/browser/workspace-task-provider.spec.js +90 -1
  115. package/lib/browser/workspace-task-provider.spec.js.map +1 -1
  116. package/lib/common/ai-ide-preferences.d.ts +1 -1
  117. package/lib/common/ai-ide-preferences.d.ts.map +1 -1
  118. package/lib/common/ai-ide-preferences.js +6 -6
  119. package/lib/common/ai-ide-preferences.js.map +1 -1
  120. package/lib/common/coder-replace-prompt-template.d.ts.map +1 -1
  121. package/lib/common/coder-replace-prompt-template.js +133 -17
  122. package/lib/common/coder-replace-prompt-template.js.map +1 -1
  123. package/lib/common/command-chat-agents.d.ts +1 -0
  124. package/lib/common/command-chat-agents.d.ts.map +1 -1
  125. package/lib/common/command-chat-agents.js +1 -0
  126. package/lib/common/command-chat-agents.js.map +1 -1
  127. package/lib/common/command-prompt-template.js +1 -1
  128. package/lib/common/orchestrator-chat-agent.d.ts.map +1 -1
  129. package/lib/common/orchestrator-chat-agent.js +2 -2
  130. package/lib/common/orchestrator-chat-agent.js.map +1 -1
  131. package/lib/common/universal-chat-agent.d.ts +1 -0
  132. package/lib/common/universal-chat-agent.d.ts.map +1 -1
  133. package/lib/common/universal-chat-agent.js +1 -0
  134. package/lib/common/universal-chat-agent.js.map +1 -1
  135. package/lib/common/universal-prompt-template.js +1 -1
  136. package/lib/common/user-interaction-tool.d.ts +53 -0
  137. package/lib/common/user-interaction-tool.d.ts.map +1 -0
  138. package/lib/common/user-interaction-tool.js +176 -0
  139. package/lib/common/user-interaction-tool.js.map +1 -0
  140. package/lib/common/user-interaction-tool.spec.d.ts +2 -0
  141. package/lib/common/user-interaction-tool.spec.d.ts.map +1 -0
  142. package/lib/common/user-interaction-tool.spec.js +216 -0
  143. package/lib/common/user-interaction-tool.spec.js.map +1 -0
  144. package/package.json +27 -27
  145. package/src/browser/agent-mode-confirmation-service.ts +19 -18
  146. package/src/browser/ai-configuration/agent-configuration-widget.tsx +2 -2
  147. package/src/browser/ai-configuration/ai-configuration-view-contribution.ts +1 -1
  148. package/src/browser/ai-configuration/prompt-fragments-configuration-widget.tsx +2 -1
  149. package/src/browser/ai-configuration/skills-configuration-widget.tsx +2 -1
  150. package/src/browser/ai-ide-activation-service.ts +43 -3
  151. package/src/browser/ai-workspace-restriction-contribution.ts +39 -0
  152. package/src/browser/app-tester-prompt-template.ts +1 -1
  153. package/src/browser/architect-agent.ts +6 -3
  154. package/src/browser/architect-prompt-template.ts +3 -3
  155. package/src/browser/chat-session-card-action-contribution.ts +1 -1
  156. package/src/browser/chat-sessions-welcome-message-provider.tsx +11 -2
  157. package/src/browser/code-reviewer-agent.ts +1 -0
  158. package/src/browser/coder-agent.ts +31 -4
  159. package/src/browser/create-skill-agent.ts +1 -0
  160. package/src/browser/explore-agent.ts +1 -0
  161. package/src/browser/file-changeset-functions.ts +17 -8
  162. package/src/browser/frontend-module.ts +14 -12
  163. package/src/browser/github-capability-contribution.ts +1 -1
  164. package/src/browser/github-prompt-template.ts +1 -1
  165. package/src/browser/ide-chat-welcome-message-provider.tsx +53 -0
  166. package/src/browser/project-info-agent.ts +1 -1
  167. package/src/browser/{context-reviewer-agent.ts → review/pr-review-agent.ts} +13 -11
  168. package/src/browser/review/pr-review-prompt-template.ts +449 -0
  169. package/src/browser/style/index.css +299 -0
  170. package/src/browser/template-preference-contribution.ts +40 -14
  171. package/src/browser/todo-tool-renderer.tsx +1 -1
  172. package/src/browser/todo-tool.spec.ts +49 -0
  173. package/src/browser/todo-tool.ts +35 -14
  174. package/src/browser/user-interaction-tool-renderer.tsx +531 -0
  175. package/src/browser/user-interaction-tool.spec.ts +396 -0
  176. package/src/browser/user-interaction-tool.ts +423 -0
  177. package/src/browser/workspace-functions.ts +10 -3
  178. package/src/browser/workspace-launch-provider.spec.ts +4 -4
  179. package/src/browser/workspace-launch-provider.ts +10 -6
  180. package/src/browser/workspace-task-provider.spec.ts +119 -1
  181. package/src/browser/workspace-task-provider.ts +4 -1
  182. package/src/common/ai-ide-preferences.ts +7 -7
  183. package/src/common/coder-replace-prompt-template.ts +133 -17
  184. package/src/common/command-chat-agents.ts +1 -0
  185. package/src/common/command-prompt-template.ts +1 -1
  186. package/src/common/orchestrator-chat-agent.ts +2 -2
  187. package/src/common/universal-chat-agent.ts +1 -0
  188. package/src/common/universal-prompt-template.ts +1 -1
  189. package/src/common/user-interaction-tool.spec.ts +241 -0
  190. package/src/common/user-interaction-tool.ts +237 -0
  191. package/lib/browser/context-reviewer-agent.d.ts +0 -17
  192. package/lib/browser/context-reviewer-agent.d.ts.map +0 -1
  193. package/lib/browser/context-reviewer-agent.js +0 -45
  194. package/lib/browser/context-reviewer-agent.js.map +0 -1
  195. package/lib/browser/context-reviewer-prompt-template.d.ts +0 -4
  196. package/lib/browser/context-reviewer-prompt-template.d.ts.map +0 -1
  197. package/lib/browser/context-reviewer-prompt-template.js +0 -160
  198. package/lib/browser/context-reviewer-prompt-template.js.map +0 -1
  199. package/lib/browser/junior-agent.d.ts.map +0 -1
  200. package/lib/browser/junior-agent.js.map +0 -1
  201. package/lib/browser/junior-plan-capability-contribution.d.ts +0 -8
  202. package/lib/browser/junior-plan-capability-contribution.d.ts.map +0 -1
  203. package/lib/browser/junior-plan-capability-contribution.js +0 -131
  204. package/lib/browser/junior-plan-capability-contribution.js.map +0 -1
  205. package/lib/browser/junior-prompt-template.d.ts +0 -4
  206. package/lib/browser/junior-prompt-template.d.ts.map +0 -1
  207. package/lib/browser/junior-prompt-template.js +0 -149
  208. package/lib/browser/junior-prompt-template.js.map +0 -1
  209. package/src/browser/context-reviewer-prompt-template.ts +0 -160
  210. package/src/browser/junior-agent.ts +0 -40
  211. package/src/browser/junior-plan-capability-contribution.ts +0 -129
  212. package/src/browser/junior-prompt-template.ts +0 -149
@@ -0,0 +1,423 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource GmbH.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { ToolProvider, ToolRequest, ToolRequestParameterProperty, ToolRequestParameters } from '@theia/ai-core';
18
+ import { ToolInvocationContext } from '@theia/ai-core/lib/common/language-model';
19
+ import { DiffUris } from '@theia/core/lib/browser/diff-uris';
20
+ import { open, OpenerService } from '@theia/core/lib/browser';
21
+ import { Deferred } from '@theia/core/lib/common/promise-util';
22
+ import { MEMORY_TEXT, MEMORY_TEXT_READONLY, ResourceProvider } from '@theia/core/lib/common/resource';
23
+ import URI from '@theia/core/lib/common/uri';
24
+ import { inject, injectable } from '@theia/core/shared/inversify';
25
+ import { EditorManager } from '@theia/editor/lib/browser';
26
+ import { ScmService } from '@theia/scm/lib/browser/scm-service';
27
+ import { WorkspaceFunctionScope } from './workspace-functions';
28
+ import {
29
+ ContentRef,
30
+ USER_INTERACTION_FUNCTION_ID,
31
+ PathContentRef,
32
+ UserInteractionLink,
33
+ UserInteractionResult,
34
+ UserInteractionStep,
35
+ UserInteractionStepResult,
36
+ buildDiffLabel,
37
+ isEmptyContentRef,
38
+ parseUserInteractionArgs,
39
+ resolveContentRef
40
+ } from '../common/user-interaction-tool';
41
+
42
+ interface PendingInteraction {
43
+ deferred: Deferred<string>;
44
+ steps: UserInteractionStep[];
45
+ stepResults: UserInteractionStepResult[];
46
+ resolved: boolean;
47
+ }
48
+
49
+ // Schemas are module-level constants so they are built once at load time
50
+ // rather than reconstructed on every getTool() call. We use a single object
51
+ // schema (no oneOf/anyOf) since some providers (notably OpenAI) handle union
52
+ // types poorly. The runtime parser additionally accepts plain string paths
53
+ // as shorthand.
54
+ const CONTENT_REF_SCHEMA: ToolRequestParameterProperty = {
55
+ type: 'object',
56
+ properties: {
57
+ path: {
58
+ type: 'string',
59
+ description: 'Workspace-relative file path. Required unless "empty" is true.'
60
+ },
61
+ gitRef: {
62
+ type: 'string',
63
+ description: 'Optional git ref (branch, tag, or commit hash). Ignored when "empty" is true.'
64
+ },
65
+ line: {
66
+ type: 'number',
67
+ description: 'Optional 1-based line number to scroll to. Ignored when "empty" is true.'
68
+ },
69
+ empty: {
70
+ type: 'boolean',
71
+ description: 'Set to true to mark this side as intentionally empty (e.g., for newly added or deleted files in a diff).'
72
+ },
73
+ label: {
74
+ type: 'string',
75
+ description: 'Optional label for an empty side (e.g., "new file", "deleted"). Only used when "empty" is true.'
76
+ }
77
+ },
78
+ description: 'Content reference. Provide "path" for a real file (optionally with "gitRef" and/or "line"), '
79
+ + 'or set "empty": true to represent a missing side of a diff.'
80
+ };
81
+
82
+ const STEP_SCHEMA: ToolRequestParameterProperty = {
83
+ type: 'object',
84
+ properties: {
85
+ title: { type: 'string', description: 'A short title for this step.' },
86
+ message: { type: 'string', description: 'A markdown-formatted message to present to the user.' },
87
+ options: {
88
+ type: 'array',
89
+ items: {
90
+ type: 'object',
91
+ properties: {
92
+ text: { type: 'string', description: 'Display text for the option button.' },
93
+ value: { type: 'string', description: 'Value returned when the user selects this option.' },
94
+ description: { type: 'string', description: 'Optional longer description shown with the option.' },
95
+ buttonLabel: { type: 'string', description: 'Optional prominent button label text. Falls back to text if not provided.' }
96
+ },
97
+ required: ['text', 'value']
98
+ },
99
+ description: 'Optional buttons offered to the user for this step. Omit for purely informational steps; '
100
+ + 'a hardcoded "Next"/"Finish" button is always shown to advance.'
101
+ },
102
+ links: {
103
+ type: 'array',
104
+ items: {
105
+ type: 'object',
106
+ properties: {
107
+ ref: {
108
+ ...CONTENT_REF_SCHEMA,
109
+ description: 'Content reference for the file (or left side of a diff). '
110
+ + 'Provide "path" for a real file, or "empty": true for files that did not exist.'
111
+ },
112
+ rightRef: {
113
+ ...CONTENT_REF_SCHEMA,
114
+ description: 'Optional right-side content reference for diff views. '
115
+ + 'Provide "path" for a real file, or "empty": true for files that no longer exist.'
116
+ },
117
+ label: { type: 'string', description: 'Optional label for the link or diff tab.' },
118
+ autoOpen: {
119
+ type: 'boolean',
120
+ description: 'Whether to automatically open the file/diff when this step becomes active. Defaults to true.'
121
+ }
122
+ },
123
+ required: ['ref']
124
+ },
125
+ description: 'Optional links to files or diffs to show alongside this step.'
126
+ }
127
+ },
128
+ required: ['title', 'message']
129
+ };
130
+
131
+ const TOOL_PARAMETERS: ToolRequestParameters = {
132
+ type: 'object',
133
+ properties: {
134
+ interactions: {
135
+ type: 'array',
136
+ items: STEP_SCHEMA,
137
+ description: 'Ordered list of wizard steps. The user walks through them sequentially without a back button.'
138
+ }
139
+ },
140
+ required: ['interactions']
141
+ };
142
+
143
+ const TOOL_DESCRIPTION = 'Present an interactive interaction to the user. Each step has a title, a markdown message, optional option buttons, '
144
+ + 'and optional file/diff links that auto-open when the step is reached. '
145
+ + 'Single-step behavior: a single-step interaction with options waits for the user to pick one option, which immediately completes the interaction; '
146
+ + 'a single-step interaction without options is purely informational and is auto-completed by the tool '
147
+ + '(do not promise the user a "Finish" or "Next" button — there is none, and no comments can be entered). '
148
+ + 'Multi-step behavior: the user advances through steps with a "Next" button (or "Finish" on the last step), can navigate freely between steps, '
149
+ + 'and may add free-form comments on every step. '
150
+ + 'The tool returns a JSON string with { "completed": boolean, "steps": [{ "title", "value"?, "comments"?, "skipped"? }] }. '
151
+ + 'If the user cancels mid-interaction, the tool returns whatever has been collected so far with "completed": false. '
152
+ + 'Use this to walk users through a series of pre-determined findings or decisions in a single tool call, '
153
+ + 'or to surface a single message/diff that should be shown inline in the chat.';
154
+
155
+ @injectable()
156
+ export class UserInteractionTool implements ToolProvider {
157
+ static ID = USER_INTERACTION_FUNCTION_ID;
158
+
159
+ @inject(OpenerService)
160
+ protected readonly openerService: OpenerService;
161
+
162
+ @inject(EditorManager)
163
+ protected readonly editorManager: EditorManager;
164
+
165
+ @inject(ScmService)
166
+ protected readonly scmService: ScmService;
167
+
168
+ @inject(WorkspaceFunctionScope)
169
+ protected readonly workspaceScope: WorkspaceFunctionScope;
170
+
171
+ @inject(ResourceProvider)
172
+ protected readonly resourceProvider: ResourceProvider;
173
+
174
+ protected readonly pendingInteractions = new Map<string, PendingInteraction>();
175
+
176
+ getTool(): ToolRequest {
177
+ return {
178
+ id: UserInteractionTool.ID,
179
+ name: UserInteractionTool.ID,
180
+ providerName: 'ai-ide',
181
+ description: TOOL_DESCRIPTION,
182
+ parameters: TOOL_PARAMETERS,
183
+ handler: (argString: string, ctx) => this.handleInteraction(argString, ctx)
184
+ };
185
+ }
186
+
187
+ setStepResult(toolCallId: string, stepIndex: number, partial: Partial<UserInteractionStepResult>): void {
188
+ const pending = this.pendingInteractions.get(toolCallId);
189
+ if (!pending || pending.resolved) {
190
+ return;
191
+ }
192
+ if (stepIndex < 0 || stepIndex >= pending.steps.length) {
193
+ return;
194
+ }
195
+ const existing = pending.stepResults[stepIndex] ?? { title: pending.steps[stepIndex].title };
196
+ pending.stepResults[stepIndex] = {
197
+ ...existing,
198
+ ...partial,
199
+ title: pending.steps[stepIndex].title
200
+ };
201
+ }
202
+
203
+ completeInteraction(toolCallId: string): void {
204
+ const pending = this.pendingInteractions.get(toolCallId);
205
+ if (!pending || pending.resolved) {
206
+ return;
207
+ }
208
+ pending.resolved = true;
209
+ const result: UserInteractionResult = {
210
+ completed: true,
211
+ steps: this.normalizeStepResults(pending)
212
+ };
213
+ pending.deferred.resolve(JSON.stringify(result));
214
+ }
215
+
216
+ /**
217
+ * Set the result for a step and immediately complete the interaction.
218
+ * Use this to atomically pass the user's input value into the result, avoiding
219
+ * any reliance on synchronous state updates between `setStepResult` and `completeInteraction`.
220
+ */
221
+ completeInteractionWith(toolCallId: string, stepIndex: number, partial: Partial<UserInteractionStepResult>): void {
222
+ this.setStepResult(toolCallId, stepIndex, partial);
223
+ this.completeInteraction(toolCallId);
224
+ }
225
+
226
+ cancelInteraction(toolCallId: string): void {
227
+ const pending = this.pendingInteractions.get(toolCallId);
228
+ if (!pending || pending.resolved) {
229
+ return;
230
+ }
231
+ pending.resolved = true;
232
+ const steps = this.normalizeStepResults(pending);
233
+ // Mark steps without any user input as skipped.
234
+ for (let i = 0; i < steps.length; i++) {
235
+ const step = steps[i];
236
+ const hasInput = step.value !== undefined || (step.comments && step.comments.length > 0);
237
+ if (!hasInput) {
238
+ steps[i] = { ...step, skipped: true };
239
+ }
240
+ }
241
+ const result: UserInteractionResult = { completed: false, steps };
242
+ pending.deferred.resolve(JSON.stringify(result));
243
+ }
244
+
245
+ protected normalizeStepResults(pending: PendingInteraction): UserInteractionStepResult[] {
246
+ return pending.steps.map((step, i) => pending.stepResults[i] ?? { title: step.title });
247
+ }
248
+
249
+ async openLink(link: UserInteractionLink): Promise<void> {
250
+ const workspaceRoot = await this.workspaceScope.getWorkspaceRoot();
251
+
252
+ if (link.rightRef !== undefined) {
253
+ const resolvedLeftUri = await this.resolveDiffSideUri(link.ref, workspaceRoot);
254
+ const resolvedRightUri = await this.resolveDiffSideUri(link.rightRef, workspaceRoot);
255
+ const left = resolveContentRef(link.ref);
256
+ const right = resolveContentRef(link.rightRef);
257
+ const diffLabel = link.label || buildDiffLabel(left, right);
258
+ const diffUri = DiffUris.encode(resolvedLeftUri, resolvedRightUri, diffLabel);
259
+ // Prefer the right-side line (working copy) since diff editors reveal
260
+ // selections on the modified editor; fall back to the left-side line.
261
+ const line = (!isEmptyContentRef(right) && right.line)
262
+ || (!isEmptyContentRef(left) && left.line)
263
+ || undefined;
264
+ const selection = line ? { start: { line: line - 1, character: 0 } } : undefined;
265
+ await open(this.openerService, diffUri, { selection });
266
+ } else {
267
+ if (isEmptyContentRef(link.ref)) {
268
+ return;
269
+ }
270
+ const left = resolveContentRef(link.ref) as PathContentRef;
271
+ if (left.gitRef) {
272
+ const uri = this.resolveUri(left, workspaceRoot);
273
+ let targetUri: URI;
274
+ if (uri === undefined) {
275
+ targetUri = this.errorContentUri(left.path, left.gitRef);
276
+ } else if (await this.canResolveUri(uri)) {
277
+ targetUri = uri;
278
+ } else {
279
+ // SCM is available but reading at this ref failed — likely the
280
+ // ref does not exist or the file did not exist at that ref.
281
+ targetUri = this.refNotFoundUri(left.path, left.gitRef);
282
+ }
283
+ await open(this.openerService, targetUri);
284
+ } else {
285
+ const fileUri = workspaceRoot.resolve(left.path);
286
+ this.workspaceScope.ensureWithinWorkspace(fileUri, workspaceRoot);
287
+ if (!(await this.canResolveUri(fileUri))) {
288
+ await open(this.openerService, this.fileNotFoundUri(left.path));
289
+ return;
290
+ }
291
+ const selection = left.line
292
+ ? { start: { line: left.line - 1, character: 0 } }
293
+ : undefined;
294
+ await this.editorManager.open(fileUri, { selection });
295
+ }
296
+ }
297
+ }
298
+
299
+ protected resolveUri(
300
+ ref: PathContentRef,
301
+ workspaceRoot: URI
302
+ ): URI | undefined {
303
+ const fileUri = workspaceRoot.resolve(ref.path);
304
+ this.workspaceScope.ensureWithinWorkspace(fileUri, workspaceRoot);
305
+ if (ref.gitRef) {
306
+ const repo = this.scmService.findRepository(fileUri);
307
+ if (repo) {
308
+ const query = { path: fileUri['codeUri'].fsPath, ref: ref.gitRef };
309
+ return fileUri.withScheme(repo.provider.id).withQuery(JSON.stringify(query));
310
+ }
311
+ console.warn(`No SCM repository found to resolve gitRef '${ref.gitRef}' for '${ref.path}'`);
312
+ return undefined;
313
+ }
314
+ return fileUri;
315
+ }
316
+
317
+ protected async handleInteraction(argString: string, ctx: ToolInvocationContext | undefined): Promise<string> {
318
+ try {
319
+ JSON.parse(argString);
320
+ } catch {
321
+ return JSON.stringify({ error: 'Invalid arguments' });
322
+ }
323
+ // Validate via the shared parser so the tool only ever waits for steps that
324
+ // the renderer would actually render. Otherwise the agent could send a
325
+ // step the UI filters out, leaving the tool blocked on input forever.
326
+ const validated = parseUserInteractionArgs(argString);
327
+ if (!validated || validated.interactions.length === 0) {
328
+ return JSON.stringify({ error: 'No interactions provided' });
329
+ }
330
+
331
+ const toolCallId = ToolInvocationContext.getToolCallId(ctx);
332
+ if (!toolCallId) {
333
+ return JSON.stringify({ error: 'No tool call ID available' });
334
+ }
335
+
336
+ const steps: UserInteractionStep[] = validated.interactions;
337
+
338
+ // Single-step interactions without options are purely informational
339
+ // (message + optional links/diffs) and should not block the agent.
340
+ // Resolve immediately with completed=true.
341
+ if (steps.length === 1 && (!steps[0].options || steps[0].options.length === 0)) {
342
+ const result: UserInteractionResult = {
343
+ completed: true,
344
+ steps: [{ title: steps[0].title }]
345
+ };
346
+ return JSON.stringify(result);
347
+ }
348
+
349
+ const pending: PendingInteraction = {
350
+ deferred: new Deferred<string>(),
351
+ steps,
352
+ stepResults: new Array(steps.length),
353
+ resolved: false
354
+ };
355
+ this.pendingInteractions.set(toolCallId, pending);
356
+
357
+ const cancellationToken = ToolInvocationContext.getCancellationToken(ctx);
358
+ const cancellationListener = cancellationToken?.onCancellationRequested(() => this.cancelInteraction(toolCallId));
359
+
360
+ try {
361
+ return await pending.deferred.promise;
362
+ } finally {
363
+ cancellationListener?.dispose();
364
+ this.pendingInteractions.delete(toolCallId);
365
+ }
366
+ }
367
+
368
+ protected async resolveDiffSideUri(ref: ContentRef, workspaceRoot: URI): Promise<URI> {
369
+ if (isEmptyContentRef(ref)) {
370
+ return this.emptyContentUri(ref.label || '');
371
+ }
372
+ const resolved = resolveContentRef(ref) as PathContentRef;
373
+ const uri = this.resolveUri(resolved, workspaceRoot);
374
+ if (uri === undefined) {
375
+ // No SCM provider could resolve the gitRef — surface as an actionable error.
376
+ return this.errorContentUri(resolved.path, resolved.gitRef!);
377
+ }
378
+ if (await this.canResolveUri(uri)) {
379
+ return uri;
380
+ }
381
+ // SCM resolved the URI but reading content failed — most commonly the file
382
+ // simply did not exist at that revision (e.g. newly added file). Show empty.
383
+ return this.emptyContentUri(resolved.path);
384
+ }
385
+
386
+ protected errorContentUri(path: string, gitRef: string): URI {
387
+ const message = `Unable to resolve revision '${gitRef}' for '${path}'.\n\n`
388
+ + 'No SCM provider is available to retrieve this revision. '
389
+ + 'Ensure the Git extension is active and the repository is recognized.';
390
+ return new URI().withScheme(MEMORY_TEXT_READONLY).withPath(path).withQuery(message);
391
+ }
392
+
393
+ protected refNotFoundUri(path: string, gitRef: string): URI {
394
+ const message = `Could not load '${path}' at revision '${gitRef}'.\n\n`
395
+ + 'The revision may not exist, or the file did not exist at that revision.';
396
+ return new URI().withScheme(MEMORY_TEXT_READONLY).withPath(path).withQuery(message);
397
+ }
398
+
399
+ protected fileNotFoundUri(path: string): URI {
400
+ const message = `Could not load '${path}'.\n\nThe file does not exist in the current workspace.`;
401
+ return new URI().withScheme(MEMORY_TEXT_READONLY).withPath(path).withQuery(message);
402
+ }
403
+
404
+ protected emptyContentUri(path: string): URI {
405
+ return new URI().withScheme(MEMORY_TEXT).withPath(path);
406
+ }
407
+
408
+ protected async canResolveUri(uri: URI): Promise<boolean> {
409
+ try {
410
+ const resource = await this.resourceProvider(uri);
411
+ try {
412
+ await resource.readContents();
413
+ return true;
414
+ } catch {
415
+ return false;
416
+ } finally {
417
+ resource.dispose();
418
+ }
419
+ } catch {
420
+ return false;
421
+ }
422
+ }
423
+ }
@@ -24,6 +24,7 @@ import {
24
24
  GET_WORKSPACE_DIRECTORY_STRUCTURE_FUNCTION_ID,
25
25
  GET_WORKSPACE_FILE_LIST_FUNCTION_ID, FIND_FILES_BY_PATTERN_FUNCTION_ID
26
26
  } from '../common/workspace-functions';
27
+ import { extractJsonStringField } from '@theia/ai-chat-ui/lib/browser/chat-response-renderer/toolcall-utils';
27
28
  import ignore from 'ignore';
28
29
  import { Minimatch } from 'minimatch';
29
30
  import { CONSIDER_GITIGNORE_PREF, FILE_CONTENT_MAX_SIZE_KB_PREF, USER_EXCLUDE_PATTERN_PREF } from '../common/workspace-preferences';
@@ -319,7 +320,10 @@ export class FileContentFunction implements ToolProvider {
319
320
  return { label: String(parsed.file), hasMore };
320
321
  }
321
322
  } catch {
322
- // ignore parse errors
323
+ const file = extractJsonStringField(args, 'file');
324
+ if (file) {
325
+ return { label: file, hasMore: false };
326
+ }
323
327
  }
324
328
  return undefined;
325
329
  },
@@ -451,7 +455,7 @@ export class FileContentFunction implements ToolProvider {
451
455
  } catch (e) {
452
456
  if (e instanceof FileOperationError &&
453
457
  (e.fileOperationResult === FileOperationResult.FILE_TOO_LARGE ||
454
- e.fileOperationResult === FileOperationResult.FILE_EXCEEDS_MEMORY_LIMIT)) {
458
+ e.fileOperationResult === FileOperationResult.FILE_EXCEEDS_MEMORY_LIMIT)) {
455
459
  return JSON.stringify({
456
460
  error: 'File exceeds the configured ' + maxSizeKB + 'KB size limit. ' +
457
461
  'Use the \'offset\' (0-based) and \'limit\' parameters to read specific line ranges, ' +
@@ -837,7 +841,10 @@ export class FindFilesByPattern implements ToolProvider {
837
841
  return { label: String(parsed.pattern), hasMore: keys.length > 1 };
838
842
  }
839
843
  } catch {
840
- // ignore parse errors
844
+ const pattern = extractJsonStringField(args, 'pattern');
845
+ if (pattern) {
846
+ return { label: pattern, hasMore: false };
847
+ }
841
848
  }
842
849
  return undefined;
843
850
  },
@@ -144,12 +144,12 @@ describe('Launch Management Tool Providers', () => {
144
144
  expect(tool.description).to.contain(
145
145
  'Lists available launch configurations'
146
146
  );
147
- expect(tool.parameters.required).to.deep.equal(['filter']);
147
+ expect(tool.parameters.required).to.deep.equal([]);
148
148
  });
149
149
 
150
- it('should list all configurations without filter', async () => {
150
+ it('should list all configurations when filter is omitted', async () => {
151
151
  const tool = launchListProvider.getTool();
152
- const result = await tool.handler('{"filter":""}');
152
+ const result = await tool.handler('{}');
153
153
  expect(result).to.be.a('string');
154
154
  const configurations = JSON.parse(result as string);
155
155
 
@@ -195,7 +195,7 @@ describe('Launch Management Tool Providers', () => {
195
195
  expect(tool.id).to.equal('runLaunchConfiguration');
196
196
  expect(tool.name).to.equal('runLaunchConfiguration');
197
197
  expect(tool.description).to.contain(
198
- 'Executes a specified launch configuration'
198
+ 'Starts a launch configuration'
199
199
  );
200
200
  expect(tool.parameters.required).to.deep.equal([
201
201
  'configurationName',
@@ -45,7 +45,9 @@ export class LaunchListProvider implements ToolProvider {
45
45
  return {
46
46
  id: LIST_LAUNCH_CONFIGURATIONS_FUNCTION_ID,
47
47
  name: LIST_LAUNCH_CONFIGURATIONS_FUNCTION_ID,
48
- description: 'Lists available launch configurations in the workspace. Launch configurations can be filtered by name. Each configuration includes its running status.',
48
+ description: 'Lists available launch configurations in the workspace. Each result includes the configuration name and whether it is currently running. ' +
49
+ 'Optionally provide a filter substring to narrow results by name. If omitted, all configurations are returned. ' +
50
+ 'Always call this before runLaunchConfiguration to discover exact configuration names.',
49
51
  parameters: {
50
52
  type: 'object',
51
53
  properties: {
@@ -54,10 +56,10 @@ export class LaunchListProvider implements ToolProvider {
54
56
  description: 'Filter to apply on launch configuration names (empty string to retrieve all configurations).'
55
57
  }
56
58
  },
57
- required: ['filter']
59
+ required: []
58
60
  },
59
61
  handler: async (argString: string) => {
60
- const filterArgs: { filter: string } = JSON.parse(argString);
62
+ const filterArgs: { filter?: string } = JSON.parse(argString);
61
63
  const configurations = await this.getAvailableLaunchConfigurations(filterArgs.filter);
62
64
  return JSON.stringify(configurations);
63
65
  }
@@ -70,7 +72,6 @@ export class LaunchListProvider implements ToolProvider {
70
72
  const runningSessions = new Set(
71
73
  this.debugSessionManager.sessions.map(session => session.configuration.name)
72
74
  );
73
-
74
75
  for (const options of this.debugConfigurationManager.all) {
75
76
  const name = this.getDisplayName(options);
76
77
  if (name.toLowerCase().includes(filter.toLowerCase())) {
@@ -107,7 +108,9 @@ export class LaunchRunnerProvider implements ToolProvider {
107
108
  return {
108
109
  id: RUN_LAUNCH_CONFIGURATION_FUNCTION_ID,
109
110
  name: RUN_LAUNCH_CONFIGURATION_FUNCTION_ID,
110
- description: 'Executes a specified launch configuration to start debugging.',
111
+ description: 'Starts a launch configuration and returns immediately — the application continues running in the background. ' +
112
+ 'Use listLaunchConfigurations first to discover available configuration names and check whether one is already running. ' +
113
+ 'The response includes the debug session ID on success. If the configuration name doesn\'t match any available configuration, returns an error.',
111
114
  parameters: {
112
115
  type: 'object',
113
116
  properties: {
@@ -189,7 +192,8 @@ export class LaunchStopProvider implements ToolProvider {
189
192
  return {
190
193
  id: STOP_LAUNCH_CONFIGURATION_FUNCTION_ID,
191
194
  name: STOP_LAUNCH_CONFIGURATION_FUNCTION_ID,
192
- description: 'Stops an active launch configuration or debug session.',
195
+ description: 'Stops an active launch configuration or debug session. If a configuration name is provided, stops the session matching that name. ' +
196
+ 'If no name is provided, stops the currently active session. Returns an error if no matching active session is found.',
193
197
  parameters: {
194
198
  type: 'object',
195
199
  properties: {
@@ -74,7 +74,8 @@ describe('Workspace Task Provider Cancellation Tests', () => {
74
74
  terminateTask: async (activeTaskInfo: TaskInfo) => {
75
75
  // Track termination
76
76
  },
77
- getTerminateSignal: async () => 'SIGTERM'
77
+ getTerminateSignal: async () => 'SIGTERM',
78
+ isTaskRunning: () => false
78
79
  } as unknown as TaskService;
79
80
 
80
81
  mockTerminalService = {
@@ -98,6 +99,123 @@ describe('Workspace Task Provider Cancellation Tests', () => {
98
99
  cancellationTokenSource.dispose();
99
100
  });
100
101
 
102
+ describe('Task cancellation with completed tasks', () => {
103
+ it('should NOT terminate task if task has already completed (not in runningTasks)', async () => {
104
+ let terminateTaskCalled = false;
105
+ mockTaskService.terminateTask = async () => {
106
+ terminateTaskCalled = true;
107
+ };
108
+ // Simulate task already completed (isTaskRunning returns false)
109
+ mockTaskService.isTaskRunning = () => false;
110
+
111
+ // Mock getTerminateSignal to never resolve (simulates in-flight handler)
112
+ mockTaskService.getTerminateSignal = () => new Promise(() => { });
113
+
114
+ const taskRunnerProvider = container.get(TaskRunnerProvider);
115
+ const handler = taskRunnerProvider.getTool().handler;
116
+
117
+ // Start task execution (will hang on getTerminateSignal)
118
+ handler(JSON.stringify({ taskName: 'build' }), mockCtx);
119
+
120
+ // Give time for the handler to register the cancellation listener
121
+ await new Promise(resolve => setTimeout(resolve, 10));
122
+
123
+ // Cancel while handler is "in-flight"
124
+ cancellationTokenSource.cancel();
125
+
126
+ // Give time for cancellation to process
127
+ await new Promise(resolve => setTimeout(resolve, 10));
128
+
129
+ // terminateTask should NOT have been called since isTaskRunning returns false
130
+ expect(terminateTaskCalled).to.be.false;
131
+ });
132
+
133
+ it('should terminate task if task is still running', async () => {
134
+ let terminateTaskCalled = false;
135
+ mockTaskService.terminateTask = async () => {
136
+ terminateTaskCalled = true;
137
+ };
138
+
139
+ // Mock isTaskRunning to return true (task still running)
140
+ mockTaskService.isTaskRunning = () => true;
141
+
142
+ // Mock getTerminateSignal to never resolve (simulates in-flight task)
143
+ mockTaskService.getTerminateSignal = () => new Promise(() => { });
144
+
145
+ const taskRunnerProvider = container.get(TaskRunnerProvider);
146
+ const handler = taskRunnerProvider.getTool().handler;
147
+
148
+ // Start task execution (will hang on getTerminateSignal)
149
+ handler(JSON.stringify({ taskName: 'build' }), mockCtx);
150
+
151
+ // Give time for the handler to register the cancellation listener
152
+ await new Promise(resolve => setTimeout(resolve, 10));
153
+
154
+ // Cancel while task is "in-flight"
155
+ cancellationTokenSource.cancel();
156
+
157
+ // Give time for cancellation to process
158
+ await new Promise(resolve => setTimeout(resolve, 10));
159
+
160
+ // terminateTask SHOULD have been called since isTaskRunning returns true
161
+ expect(terminateTaskCalled).to.be.true;
162
+ });
163
+
164
+ it('should handle multiple tasks with shared cancellation token - only terminate running tasks', async () => {
165
+ const terminatedTasks: number[] = [];
166
+ mockTaskService.terminateTask = async (taskInfo: TaskInfo) => {
167
+ terminatedTasks.push(taskInfo.taskId);
168
+ };
169
+
170
+ // Mock isTaskRunning to simulate: task 0 completed, tasks 1 & 2 still running
171
+ mockTaskService.isTaskRunning = (taskId: number) => taskId !== 0;
172
+
173
+ // Mock getTerminateSignal to never resolve (simulates in-flight handlers)
174
+ mockTaskService.getTerminateSignal = () => new Promise(() => { });
175
+
176
+ // Mock runTaskByLabel to return different task IDs
177
+ let taskIdCounter = 0;
178
+ mockTaskService.runTaskByLabel = async (token: number, taskLabel: string) => ({
179
+ taskId: taskIdCounter++,
180
+ terminalId: taskIdCounter - 1,
181
+ config: {
182
+ label: taskLabel,
183
+ _scope: 'workspace',
184
+ type: 'shell'
185
+ }
186
+ } as TaskInfo);
187
+
188
+ const taskRunnerProvider = container.get(TaskRunnerProvider);
189
+ const handler = taskRunnerProvider.getTool().handler;
190
+
191
+ // Use ONE shared CancellationTokenSource (simulates real scenario)
192
+ const sharedCts = new CancellationTokenSource();
193
+ const sharedCtx: ToolInvocationContext = { cancellationToken: sharedCts.token };
194
+
195
+ // Call handler three times with the same shared cancellation token
196
+ handler(JSON.stringify({ taskName: 'build' }), sharedCtx);
197
+ handler(JSON.stringify({ taskName: 'test' }), sharedCtx);
198
+ handler(JSON.stringify({ taskName: 'lint' }), sharedCtx);
199
+
200
+ // Give time for handlers to register cancellation listeners
201
+ await new Promise(resolve => setTimeout(resolve, 10));
202
+
203
+ // Cancel the shared token once - this fires all registered listeners
204
+ sharedCts.cancel();
205
+
206
+ // Give time for cancellation listeners to fire
207
+ await new Promise(resolve => setTimeout(resolve, 10));
208
+
209
+ // Only task 1 and 2 should have been terminated (task 0 was completed)
210
+ expect(terminatedTasks).to.have.lengthOf(2);
211
+ expect(terminatedTasks).to.include(1);
212
+ expect(terminatedTasks).to.include(2);
213
+ expect(terminatedTasks).to.not.include(0);
214
+
215
+ sharedCts.dispose();
216
+ });
217
+ });
218
+
101
219
  it('TaskListProvider should respect cancellation token', async () => {
102
220
  const taskListProvider = container.get(TaskListProvider);
103
221
  cancellationTokenSource.cancel();