@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.
- package/lib/browser/agent-mode-confirmation-service.d.ts.map +1 -1
- package/lib/browser/agent-mode-confirmation-service.js +15 -15
- package/lib/browser/agent-mode-confirmation-service.js.map +1 -1
- package/lib/browser/ai-configuration/agent-configuration-widget.js +2 -2
- package/lib/browser/ai-configuration/agent-configuration-widget.js.map +1 -1
- package/lib/browser/ai-configuration/ai-configuration-view-contribution.js +1 -1
- package/lib/browser/ai-configuration/ai-configuration-view-contribution.js.map +1 -1
- package/lib/browser/ai-configuration/prompt-fragments-configuration-widget.d.ts.map +1 -1
- package/lib/browser/ai-configuration/prompt-fragments-configuration-widget.js +2 -1
- package/lib/browser/ai-configuration/prompt-fragments-configuration-widget.js.map +1 -1
- package/lib/browser/ai-configuration/skills-configuration-widget.d.ts.map +1 -1
- package/lib/browser/ai-configuration/skills-configuration-widget.js +2 -1
- package/lib/browser/ai-configuration/skills-configuration-widget.js.map +1 -1
- package/lib/browser/ai-ide-activation-service.d.ts +10 -1
- package/lib/browser/ai-ide-activation-service.d.ts.map +1 -1
- package/lib/browser/ai-ide-activation-service.js +41 -1
- package/lib/browser/ai-ide-activation-service.js.map +1 -1
- package/lib/browser/ai-workspace-restriction-contribution.d.ts +7 -0
- package/lib/browser/ai-workspace-restriction-contribution.d.ts.map +1 -0
- package/lib/browser/ai-workspace-restriction-contribution.js +42 -0
- package/lib/browser/ai-workspace-restriction-contribution.js.map +1 -0
- package/lib/browser/app-tester-prompt-template.js +1 -1
- package/lib/browser/architect-agent.d.ts +1 -0
- package/lib/browser/architect-agent.d.ts.map +1 -1
- package/lib/browser/architect-agent.js +5 -3
- package/lib/browser/architect-agent.js.map +1 -1
- package/lib/browser/architect-prompt-template.js +3 -3
- package/lib/browser/chat-session-card-action-contribution.js +1 -1
- package/lib/browser/chat-session-card-action-contribution.js.map +1 -1
- package/lib/browser/chat-sessions-welcome-message-provider.d.ts +1 -0
- package/lib/browser/chat-sessions-welcome-message-provider.d.ts.map +1 -1
- package/lib/browser/chat-sessions-welcome-message-provider.js +7 -1
- package/lib/browser/chat-sessions-welcome-message-provider.js.map +1 -1
- package/lib/browser/code-reviewer-agent.d.ts +1 -0
- package/lib/browser/code-reviewer-agent.d.ts.map +1 -1
- package/lib/browser/code-reviewer-agent.js +1 -0
- package/lib/browser/code-reviewer-agent.js.map +1 -1
- package/lib/browser/coder-agent.d.ts +4 -0
- package/lib/browser/coder-agent.d.ts.map +1 -1
- package/lib/browser/coder-agent.js +28 -4
- package/lib/browser/coder-agent.js.map +1 -1
- package/lib/browser/create-skill-agent.d.ts +1 -0
- package/lib/browser/create-skill-agent.d.ts.map +1 -1
- package/lib/browser/create-skill-agent.js +1 -0
- package/lib/browser/create-skill-agent.js.map +1 -1
- package/lib/browser/explore-agent.d.ts +1 -0
- package/lib/browser/explore-agent.d.ts.map +1 -1
- package/lib/browser/explore-agent.js +1 -0
- package/lib/browser/explore-agent.js.map +1 -1
- package/lib/browser/file-changeset-functions.d.ts.map +1 -1
- package/lib/browser/file-changeset-functions.js +17 -9
- package/lib/browser/file-changeset-functions.js.map +1 -1
- package/lib/browser/frontend-module.d.ts.map +1 -1
- package/lib/browser/frontend-module.js +12 -10
- package/lib/browser/frontend-module.js.map +1 -1
- package/lib/browser/github-capability-contribution.js +1 -1
- package/lib/browser/github-capability-contribution.js.map +1 -1
- package/lib/browser/github-prompt-template.js +1 -1
- package/lib/browser/ide-chat-welcome-message-provider.d.ts +4 -0
- package/lib/browser/ide-chat-welcome-message-provider.d.ts.map +1 -1
- package/lib/browser/ide-chat-welcome-message-provider.js +34 -0
- package/lib/browser/ide-chat-welcome-message-provider.js.map +1 -1
- package/lib/browser/project-info-agent.d.ts +1 -0
- package/lib/browser/project-info-agent.d.ts.map +1 -1
- package/lib/browser/project-info-agent.js +1 -0
- package/lib/browser/project-info-agent.js.map +1 -1
- package/lib/browser/{junior-agent.d.ts → review/pr-review-agent.d.ts} +7 -5
- package/lib/browser/review/pr-review-agent.d.ts.map +1 -0
- package/lib/browser/{junior-agent.js → review/pr-review-agent.js} +17 -15
- package/lib/browser/review/pr-review-agent.js.map +1 -0
- package/lib/browser/review/pr-review-prompt-template.d.ts +4 -0
- package/lib/browser/review/pr-review-prompt-template.d.ts.map +1 -0
- package/lib/browser/review/pr-review-prompt-template.js +437 -0
- package/lib/browser/review/pr-review-prompt-template.js.map +1 -0
- package/lib/browser/template-preference-contribution.d.ts +2 -0
- package/lib/browser/template-preference-contribution.d.ts.map +1 -1
- package/lib/browser/template-preference-contribution.js +43 -14
- package/lib/browser/template-preference-contribution.js.map +1 -1
- package/lib/browser/todo-tool-renderer.d.ts +1 -1
- package/lib/browser/todo-tool-renderer.d.ts.map +1 -1
- package/lib/browser/todo-tool-renderer.js +1 -1
- package/lib/browser/todo-tool-renderer.js.map +1 -1
- package/lib/browser/todo-tool.d.ts +0 -1
- package/lib/browser/todo-tool.d.ts.map +1 -1
- package/lib/browser/todo-tool.js +36 -16
- package/lib/browser/todo-tool.js.map +1 -1
- package/lib/browser/todo-tool.spec.d.ts +2 -0
- package/lib/browser/todo-tool.spec.d.ts.map +1 -0
- package/lib/browser/todo-tool.spec.js +44 -0
- package/lib/browser/todo-tool.spec.js.map +1 -0
- package/lib/browser/user-interaction-tool-renderer.d.ts +18 -0
- package/lib/browser/user-interaction-tool-renderer.d.ts.map +1 -0
- package/lib/browser/user-interaction-tool-renderer.js +330 -0
- package/lib/browser/user-interaction-tool-renderer.js.map +1 -0
- package/lib/browser/user-interaction-tool.d.ts +47 -0
- package/lib/browser/user-interaction-tool.d.ts.map +1 -0
- package/lib/browser/user-interaction-tool.js +397 -0
- package/lib/browser/user-interaction-tool.js.map +1 -0
- package/lib/browser/user-interaction-tool.spec.d.ts +2 -0
- package/lib/browser/user-interaction-tool.spec.d.ts.map +1 -0
- package/lib/browser/user-interaction-tool.spec.js +336 -0
- package/lib/browser/user-interaction-tool.spec.js.map +1 -0
- package/lib/browser/workspace-functions.d.ts.map +1 -1
- package/lib/browser/workspace-functions.js +9 -2
- package/lib/browser/workspace-functions.js.map +1 -1
- package/lib/browser/workspace-launch-provider.d.ts.map +1 -1
- package/lib/browser/workspace-launch-provider.js +9 -4
- package/lib/browser/workspace-launch-provider.js.map +1 -1
- package/lib/browser/workspace-launch-provider.spec.js +4 -4
- package/lib/browser/workspace-launch-provider.spec.js.map +1 -1
- package/lib/browser/workspace-task-provider.d.ts.map +1 -1
- package/lib/browser/workspace-task-provider.js +4 -1
- package/lib/browser/workspace-task-provider.js.map +1 -1
- package/lib/browser/workspace-task-provider.spec.js +90 -1
- package/lib/browser/workspace-task-provider.spec.js.map +1 -1
- package/lib/common/ai-ide-preferences.d.ts +1 -1
- package/lib/common/ai-ide-preferences.d.ts.map +1 -1
- package/lib/common/ai-ide-preferences.js +6 -6
- package/lib/common/ai-ide-preferences.js.map +1 -1
- package/lib/common/coder-replace-prompt-template.d.ts.map +1 -1
- package/lib/common/coder-replace-prompt-template.js +133 -17
- package/lib/common/coder-replace-prompt-template.js.map +1 -1
- package/lib/common/command-chat-agents.d.ts +1 -0
- package/lib/common/command-chat-agents.d.ts.map +1 -1
- package/lib/common/command-chat-agents.js +1 -0
- package/lib/common/command-chat-agents.js.map +1 -1
- package/lib/common/command-prompt-template.js +1 -1
- package/lib/common/orchestrator-chat-agent.d.ts.map +1 -1
- package/lib/common/orchestrator-chat-agent.js +2 -2
- package/lib/common/orchestrator-chat-agent.js.map +1 -1
- package/lib/common/universal-chat-agent.d.ts +1 -0
- package/lib/common/universal-chat-agent.d.ts.map +1 -1
- package/lib/common/universal-chat-agent.js +1 -0
- package/lib/common/universal-chat-agent.js.map +1 -1
- package/lib/common/universal-prompt-template.js +1 -1
- package/lib/common/user-interaction-tool.d.ts +53 -0
- package/lib/common/user-interaction-tool.d.ts.map +1 -0
- package/lib/common/user-interaction-tool.js +176 -0
- package/lib/common/user-interaction-tool.js.map +1 -0
- package/lib/common/user-interaction-tool.spec.d.ts +2 -0
- package/lib/common/user-interaction-tool.spec.d.ts.map +1 -0
- package/lib/common/user-interaction-tool.spec.js +216 -0
- package/lib/common/user-interaction-tool.spec.js.map +1 -0
- package/package.json +27 -27
- package/src/browser/agent-mode-confirmation-service.ts +19 -18
- package/src/browser/ai-configuration/agent-configuration-widget.tsx +2 -2
- package/src/browser/ai-configuration/ai-configuration-view-contribution.ts +1 -1
- package/src/browser/ai-configuration/prompt-fragments-configuration-widget.tsx +2 -1
- package/src/browser/ai-configuration/skills-configuration-widget.tsx +2 -1
- package/src/browser/ai-ide-activation-service.ts +43 -3
- package/src/browser/ai-workspace-restriction-contribution.ts +39 -0
- package/src/browser/app-tester-prompt-template.ts +1 -1
- package/src/browser/architect-agent.ts +6 -3
- package/src/browser/architect-prompt-template.ts +3 -3
- package/src/browser/chat-session-card-action-contribution.ts +1 -1
- package/src/browser/chat-sessions-welcome-message-provider.tsx +11 -2
- package/src/browser/code-reviewer-agent.ts +1 -0
- package/src/browser/coder-agent.ts +31 -4
- package/src/browser/create-skill-agent.ts +1 -0
- package/src/browser/explore-agent.ts +1 -0
- package/src/browser/file-changeset-functions.ts +17 -8
- package/src/browser/frontend-module.ts +14 -12
- package/src/browser/github-capability-contribution.ts +1 -1
- package/src/browser/github-prompt-template.ts +1 -1
- package/src/browser/ide-chat-welcome-message-provider.tsx +53 -0
- package/src/browser/project-info-agent.ts +1 -1
- package/src/browser/{context-reviewer-agent.ts → review/pr-review-agent.ts} +13 -11
- package/src/browser/review/pr-review-prompt-template.ts +449 -0
- package/src/browser/style/index.css +299 -0
- package/src/browser/template-preference-contribution.ts +40 -14
- package/src/browser/todo-tool-renderer.tsx +1 -1
- package/src/browser/todo-tool.spec.ts +49 -0
- package/src/browser/todo-tool.ts +35 -14
- package/src/browser/user-interaction-tool-renderer.tsx +531 -0
- package/src/browser/user-interaction-tool.spec.ts +396 -0
- package/src/browser/user-interaction-tool.ts +423 -0
- package/src/browser/workspace-functions.ts +10 -3
- package/src/browser/workspace-launch-provider.spec.ts +4 -4
- package/src/browser/workspace-launch-provider.ts +10 -6
- package/src/browser/workspace-task-provider.spec.ts +119 -1
- package/src/browser/workspace-task-provider.ts +4 -1
- package/src/common/ai-ide-preferences.ts +7 -7
- package/src/common/coder-replace-prompt-template.ts +133 -17
- package/src/common/command-chat-agents.ts +1 -0
- package/src/common/command-prompt-template.ts +1 -1
- package/src/common/orchestrator-chat-agent.ts +2 -2
- package/src/common/universal-chat-agent.ts +1 -0
- package/src/common/universal-prompt-template.ts +1 -1
- package/src/common/user-interaction-tool.spec.ts +241 -0
- package/src/common/user-interaction-tool.ts +237 -0
- package/lib/browser/context-reviewer-agent.d.ts +0 -17
- package/lib/browser/context-reviewer-agent.d.ts.map +0 -1
- package/lib/browser/context-reviewer-agent.js +0 -45
- package/lib/browser/context-reviewer-agent.js.map +0 -1
- package/lib/browser/context-reviewer-prompt-template.d.ts +0 -4
- package/lib/browser/context-reviewer-prompt-template.d.ts.map +0 -1
- package/lib/browser/context-reviewer-prompt-template.js +0 -160
- package/lib/browser/context-reviewer-prompt-template.js.map +0 -1
- package/lib/browser/junior-agent.d.ts.map +0 -1
- package/lib/browser/junior-agent.js.map +0 -1
- package/lib/browser/junior-plan-capability-contribution.d.ts +0 -8
- package/lib/browser/junior-plan-capability-contribution.d.ts.map +0 -1
- package/lib/browser/junior-plan-capability-contribution.js +0 -131
- package/lib/browser/junior-plan-capability-contribution.js.map +0 -1
- package/lib/browser/junior-prompt-template.d.ts +0 -4
- package/lib/browser/junior-prompt-template.d.ts.map +0 -1
- package/lib/browser/junior-prompt-template.js +0 -149
- package/lib/browser/junior-prompt-template.js.map +0 -1
- package/src/browser/context-reviewer-prompt-template.ts +0 -160
- package/src/browser/junior-agent.ts +0 -40
- package/src/browser/junior-plan-capability-contribution.ts +0 -129
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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([
|
|
147
|
+
expect(tool.parameters.required).to.deep.equal([]);
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
-
it('should list all configurations
|
|
150
|
+
it('should list all configurations when filter is omitted', async () => {
|
|
151
151
|
const tool = launchListProvider.getTool();
|
|
152
|
-
const result = await tool.handler('{
|
|
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
|
-
'
|
|
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.
|
|
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: [
|
|
59
|
+
required: []
|
|
58
60
|
},
|
|
59
61
|
handler: async (argString: string) => {
|
|
60
|
-
const filterArgs: { filter
|
|
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: '
|
|
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();
|