@theia/ai-ide 1.76.0-next.7 → 1.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/ai-allow-all-mode-chat-banner.js +1 -1
- package/lib/browser/ai-allow-all-mode-chat-banner.js.map +1 -1
- package/lib/browser/ai-aware-scm-commit-widget.d.ts +27 -0
- package/lib/browser/ai-aware-scm-commit-widget.d.ts.map +1 -0
- package/lib/browser/ai-aware-scm-commit-widget.js +142 -0
- package/lib/browser/ai-aware-scm-commit-widget.js.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-detail-widget.d.ts +10 -4
- package/lib/browser/ai-configuration/ai-configuration-detail-widget.d.ts.map +1 -1
- package/lib/browser/ai-configuration/ai-configuration-detail-widget.js +12 -7
- package/lib/browser/ai-configuration/ai-configuration-detail-widget.js.map +1 -1
- package/lib/browser/ai-configuration/ai-configuration-detail-widget.spec.js +14 -14
- package/lib/browser/ai-configuration/ai-configuration-detail-widget.spec.js.map +1 -1
- package/lib/browser/ai-configuration/ai-configuration-tree-widget.d.ts +6 -0
- package/lib/browser/ai-configuration/ai-configuration-tree-widget.d.ts.map +1 -1
- package/lib/browser/ai-configuration/ai-configuration-tree-widget.js +9 -0
- package/lib/browser/ai-configuration/ai-configuration-tree-widget.js.map +1 -1
- package/lib/browser/ai-configuration/categories/agent-capabilities-settings.d.ts.map +1 -1
- package/lib/browser/ai-configuration/categories/agent-capabilities-settings.js +2 -1
- package/lib/browser/ai-configuration/categories/agent-capabilities-settings.js.map +1 -1
- package/lib/browser/ai-configuration/categories/agent-chips.js +1 -1
- package/lib/browser/ai-configuration/categories/agent-chips.js.map +1 -1
- package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.d.ts +8 -0
- package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.d.ts.map +1 -1
- package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.js +27 -8
- package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.js.map +1 -1
- package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.spec.js +38 -2
- package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.spec.js.map +1 -1
- package/lib/browser/ai-configuration/categories/models-configuration-category.d.ts +13 -2
- package/lib/browser/ai-configuration/categories/models-configuration-category.d.ts.map +1 -1
- package/lib/browser/ai-configuration/categories/models-configuration-category.js +41 -2
- package/lib/browser/ai-configuration/categories/models-configuration-category.js.map +1 -1
- package/lib/browser/ai-configuration/categories/models-configuration-category.spec.js +32 -2
- package/lib/browser/ai-configuration/categories/models-configuration-category.spec.js.map +1 -1
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.d.ts +26 -0
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.d.ts.map +1 -0
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.js +212 -0
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.js.map +1 -0
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.d.ts +2 -0
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.d.ts.map +1 -0
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.js +423 -0
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.js.map +1 -0
- package/lib/browser/ai-configuration/language-model-renderer.d.ts.map +1 -1
- package/lib/browser/ai-configuration/language-model-renderer.js +10 -3
- package/lib/browser/ai-configuration/language-model-renderer.js.map +1 -1
- package/lib/browser/ai-getting-started-commands.d.ts +10 -0
- package/lib/browser/ai-getting-started-commands.d.ts.map +1 -0
- package/lib/browser/ai-getting-started-commands.js +34 -0
- package/lib/browser/ai-getting-started-commands.js.map +1 -0
- package/lib/browser/ai-getting-started-contribution.d.ts +20 -0
- package/lib/browser/ai-getting-started-contribution.d.ts.map +1 -0
- package/lib/browser/ai-getting-started-contribution.js +70 -0
- package/lib/browser/ai-getting-started-contribution.js.map +1 -0
- package/lib/browser/ai-getting-started-walkthrough.d.ts +41 -0
- package/lib/browser/ai-getting-started-walkthrough.d.ts.map +1 -0
- package/lib/browser/ai-getting-started-walkthrough.js +225 -0
- package/lib/browser/ai-getting-started-walkthrough.js.map +1 -0
- package/lib/browser/ai-getting-started-walkthrough.spec.d.ts +2 -0
- package/lib/browser/ai-getting-started-walkthrough.spec.d.ts.map +1 -0
- package/lib/browser/ai-getting-started-walkthrough.spec.js +124 -0
- package/lib/browser/ai-getting-started-walkthrough.spec.js.map +1 -0
- package/lib/browser/ai-walkthrough-context-keys.d.ts +55 -0
- package/lib/browser/ai-walkthrough-context-keys.d.ts.map +1 -0
- package/lib/browser/ai-walkthrough-context-keys.js +145 -0
- package/lib/browser/ai-walkthrough-context-keys.js.map +1 -0
- package/lib/browser/ai-walkthrough-context-keys.spec.d.ts +2 -0
- package/lib/browser/ai-walkthrough-context-keys.spec.d.ts.map +1 -0
- package/lib/browser/ai-walkthrough-context-keys.spec.js +166 -0
- package/lib/browser/ai-walkthrough-context-keys.spec.js.map +1 -0
- package/lib/browser/chat-session-item.js +2 -2
- package/lib/browser/chat-session-item.js.map +1 -1
- package/lib/browser/commit-message-agent.d.ts +31 -0
- package/lib/browser/commit-message-agent.d.ts.map +1 -0
- package/lib/browser/commit-message-agent.js +99 -0
- package/lib/browser/commit-message-agent.js.map +1 -0
- package/lib/browser/commit-message-agent.spec.d.ts +2 -0
- package/lib/browser/commit-message-agent.spec.d.ts.map +1 -0
- package/lib/browser/commit-message-agent.spec.js +82 -0
- package/lib/browser/commit-message-agent.spec.js.map +1 -0
- package/lib/browser/commit-message-command-contribution.d.ts +15 -0
- package/lib/browser/commit-message-command-contribution.d.ts.map +1 -0
- package/lib/browser/commit-message-command-contribution.js +73 -0
- package/lib/browser/commit-message-command-contribution.js.map +1 -0
- package/lib/browser/commit-message-commands.d.ts +5 -0
- package/lib/browser/commit-message-commands.d.ts.map +1 -0
- package/lib/browser/commit-message-commands.js +28 -0
- package/lib/browser/commit-message-commands.js.map +1 -0
- package/lib/browser/commit-message-prompt-template.d.ts +11 -0
- package/lib/browser/commit-message-prompt-template.d.ts.map +1 -0
- package/lib/browser/commit-message-prompt-template.js +63 -0
- package/lib/browser/commit-message-prompt-template.js.map +1 -0
- package/lib/browser/commit-message-runner.d.ts +41 -0
- package/lib/browser/commit-message-runner.d.ts.map +1 -0
- package/lib/browser/commit-message-runner.js +143 -0
- package/lib/browser/commit-message-runner.js.map +1 -0
- package/lib/browser/commit-message-runner.spec.d.ts +2 -0
- package/lib/browser/commit-message-runner.spec.d.ts.map +1 -0
- package/lib/browser/commit-message-runner.spec.js +206 -0
- package/lib/browser/commit-message-runner.spec.js.map +1 -0
- package/lib/browser/frontend-module.d.ts.map +1 -1
- package/lib/browser/frontend-module.js +25 -0
- package/lib/browser/frontend-module.js.map +1 -1
- package/lib/browser/git-changes-tool.d.ts +73 -0
- package/lib/browser/git-changes-tool.d.ts.map +1 -0
- package/lib/browser/git-changes-tool.js +217 -0
- package/lib/browser/git-changes-tool.js.map +1 -0
- package/lib/browser/git-changes-tool.spec.d.ts +2 -0
- package/lib/browser/git-changes-tool.spec.d.ts.map +1 -0
- package/lib/browser/git-changes-tool.spec.js +167 -0
- package/lib/browser/git-changes-tool.spec.js.map +1 -0
- package/lib/browser/ide-chat-welcome-message-provider.d.ts +26 -28
- package/lib/browser/ide-chat-welcome-message-provider.d.ts.map +1 -1
- package/lib/browser/ide-chat-welcome-message-provider.js +61 -151
- package/lib/browser/ide-chat-welcome-message-provider.js.map +1 -1
- package/lib/browser/review/pr-review-capability-contribution.js +1 -1
- package/lib/browser/review/pr-review-capability-contribution.js.map +1 -1
- package/lib/browser/user-interaction-tool-renderer.d.ts +6 -0
- package/lib/browser/user-interaction-tool-renderer.d.ts.map +1 -1
- package/lib/browser/user-interaction-tool-renderer.js +36 -7
- package/lib/browser/user-interaction-tool-renderer.js.map +1 -1
- package/lib/browser/user-interaction-tool.d.ts +23 -1
- package/lib/browser/user-interaction-tool.d.ts.map +1 -1
- package/lib/browser/user-interaction-tool.js +56 -17
- package/lib/browser/user-interaction-tool.js.map +1 -1
- package/lib/browser/user-interaction-tool.spec.js +151 -2
- package/lib/browser/user-interaction-tool.spec.js.map +1 -1
- package/lib/common/command-prompt-template.js +1 -1
- package/lib/common/user-interaction-tool.d.ts +19 -1
- package/lib/common/user-interaction-tool.d.ts.map +1 -1
- package/lib/common/user-interaction-tool.js +100 -29
- package/lib/common/user-interaction-tool.js.map +1 -1
- package/lib/common/user-interaction-tool.spec.js +215 -174
- package/lib/common/user-interaction-tool.spec.js.map +1 -1
- package/package.json +26 -25
- package/src/browser/ai-allow-all-mode-chat-banner.tsx +2 -2
- package/src/browser/ai-aware-scm-commit-widget.tsx +153 -0
- package/src/browser/ai-configuration/ai-configuration-detail-widget.spec.ts +14 -14
- package/src/browser/ai-configuration/ai-configuration-detail-widget.tsx +12 -7
- package/src/browser/ai-configuration/ai-configuration-tree-widget.tsx +8 -0
- package/src/browser/ai-configuration/categories/agent-capabilities-settings.tsx +7 -8
- package/src/browser/ai-configuration/categories/agent-chips.tsx +1 -1
- package/src/browser/ai-configuration/categories/model-aliases-configuration-category.spec.ts +40 -2
- package/src/browser/ai-configuration/categories/model-aliases-configuration-category.tsx +31 -10
- package/src/browser/ai-configuration/categories/models-configuration-category.spec.ts +47 -2
- package/src/browser/ai-configuration/categories/models-configuration-category.tsx +52 -5
- package/src/browser/ai-configuration/categories/provider-model-discovery-section.spec.tsx +451 -0
- package/src/browser/ai-configuration/categories/provider-model-discovery-section.tsx +341 -0
- package/src/browser/ai-configuration/language-model-renderer.tsx +11 -3
- package/src/browser/ai-getting-started-commands.ts +33 -0
- package/src/browser/ai-getting-started-contribution.ts +64 -0
- package/src/browser/ai-getting-started-walkthrough.spec.ts +146 -0
- package/src/browser/ai-getting-started-walkthrough.ts +249 -0
- package/src/browser/ai-walkthrough-context-keys.spec.ts +209 -0
- package/src/browser/ai-walkthrough-context-keys.ts +147 -0
- package/src/browser/chat-session-item.tsx +2 -2
- package/src/browser/commit-message-agent.spec.ts +97 -0
- package/src/browser/commit-message-agent.ts +113 -0
- package/src/browser/commit-message-command-contribution.ts +67 -0
- package/src/browser/commit-message-commands.ts +25 -0
- package/src/browser/commit-message-prompt-template.ts +65 -0
- package/src/browser/commit-message-runner.spec.ts +266 -0
- package/src/browser/commit-message-runner.ts +163 -0
- package/src/browser/frontend-module.ts +27 -0
- package/src/browser/git-changes-tool.spec.ts +200 -0
- package/src/browser/git-changes-tool.ts +239 -0
- package/src/browser/ide-chat-welcome-message-provider.tsx +93 -230
- package/src/browser/review/pr-review-capability-contribution.ts +1 -1
- package/src/browser/style/commit-message-overlay.css +64 -0
- package/src/browser/style/index.css +4 -42
- package/src/browser/user-interaction-tool-renderer.tsx +38 -8
- package/src/browser/user-interaction-tool.spec.ts +201 -2
- package/src/browser/user-interaction-tool.ts +69 -18
- package/src/common/command-prompt-template.ts +1 -1
- package/src/common/user-interaction-tool.spec.ts +217 -161
- package/src/common/user-interaction-tool.ts +127 -32
|
@@ -0,0 +1,163 @@
|
|
|
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 { Emitter, Event, ILogger, MessageService, nls } from '@theia/core';
|
|
18
|
+
import { ConfirmDialog, Dialog } from '@theia/core/lib/browser/dialogs';
|
|
19
|
+
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
|
|
20
|
+
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
|
21
|
+
import { ScmRepository } from '@theia/scm/lib/browser/scm-repository';
|
|
22
|
+
import { ScmService } from '@theia/scm/lib/browser/scm-service';
|
|
23
|
+
import { CommitMessageAgent } from './commit-message-agent';
|
|
24
|
+
import { GetGitChangesTool } from './git-changes-tool';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Drives the {@link CommitMessageAgent}: reads the staged git diff of the selected repository,
|
|
28
|
+
* asks the agent to turn it into a commit message and writes the result into that repository's
|
|
29
|
+
* SCM commit-message input.
|
|
30
|
+
*
|
|
31
|
+
* The diff is read by calling {@link GetGitChangesTool.getStagedChanges} directly rather than by
|
|
32
|
+
* letting the model call the tool: the agent is a plain (non-chat) agent with a single, fixed
|
|
33
|
+
* input, so there is no chat session, no tool-confirmation prompt and no chat-model bookkeeping.
|
|
34
|
+
* `git diff --cached` also renders staged additions of new files as complete addition diffs, so
|
|
35
|
+
* untracked files need no separate handling.
|
|
36
|
+
*/
|
|
37
|
+
@injectable()
|
|
38
|
+
export class CommitMessageRunner {
|
|
39
|
+
|
|
40
|
+
@inject(CommitMessageAgent)
|
|
41
|
+
protected readonly commitMessageAgent: CommitMessageAgent;
|
|
42
|
+
|
|
43
|
+
@inject(GetGitChangesTool)
|
|
44
|
+
protected readonly gitChangesTool: GetGitChangesTool;
|
|
45
|
+
|
|
46
|
+
@inject(ScmService)
|
|
47
|
+
protected readonly scmService: ScmService;
|
|
48
|
+
|
|
49
|
+
@inject(MessageService)
|
|
50
|
+
protected readonly messageService: MessageService;
|
|
51
|
+
|
|
52
|
+
@inject(ILogger) @named('ai-ide:CommitMessageRunner')
|
|
53
|
+
protected readonly logger: ILogger;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Cancellation source of the in-flight generation, or `undefined` when idle. It is fired by
|
|
57
|
+
* {@link cancel} so a click on the spinning button cancels the run even while we are still in
|
|
58
|
+
* the pre-request phase (resolving the repository, fetching the diff, awaiting the overwrite
|
|
59
|
+
* confirmation). Once the LLM request is under way, the same token cancels it via
|
|
60
|
+
* {@link CommitMessageAgent.generateCommitMessage}.
|
|
61
|
+
*/
|
|
62
|
+
protected current: CancellationTokenSource | undefined;
|
|
63
|
+
|
|
64
|
+
protected readonly onDidChangeEmitter = new Emitter<void>();
|
|
65
|
+
readonly onDidChange: Event<void> = this.onDidChangeEmitter.event;
|
|
66
|
+
|
|
67
|
+
isRunning(): boolean {
|
|
68
|
+
return !!this.current;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
cancel(): void {
|
|
72
|
+
this.current?.cancel();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async run(): Promise<void> {
|
|
76
|
+
if (this.current) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const cts = new CancellationTokenSource();
|
|
80
|
+
this.current = cts;
|
|
81
|
+
this.onDidChangeEmitter.fire();
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const repository = this.scmService.selectedRepository;
|
|
85
|
+
if (!repository) {
|
|
86
|
+
// Deliberately not awaited: a dismissed toast never resolves, which would leave
|
|
87
|
+
// the button spinning forever.
|
|
88
|
+
this.messageService.warn(
|
|
89
|
+
nls.localize('theia/ai-ide/commit-message/no-repository', 'No source-control repository is selected.')
|
|
90
|
+
);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const changes = await this.gitChangesTool.getStagedChanges(repository, cts.token);
|
|
95
|
+
if (cts.token.isCancellationRequested) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (!changes.trim()) {
|
|
99
|
+
this.messageService.warn(
|
|
100
|
+
nls.localize('theia/ai-ide/commit-message/no-changes', 'There are no staged changes to generate a commit message from.')
|
|
101
|
+
);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!(await this.confirmOverwrite(repository))) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (cts.token.isCancellationRequested) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const message = await this.commitMessageAgent.generateCommitMessage(changes, cts.token);
|
|
113
|
+
if (cts.token.isCancellationRequested) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!message) {
|
|
117
|
+
this.messageService.warn(
|
|
118
|
+
nls.localize('theia/ai-ide/commit-message/empty', 'The model returned an empty commit message.')
|
|
119
|
+
);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
repository.input.value = message;
|
|
124
|
+
repository.input.focus();
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (cts.token.isCancellationRequested) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
this.logger.error('Failed to run commit-message agent', error);
|
|
130
|
+
this.notifyFailure(error instanceof Error ? error.message : String(error));
|
|
131
|
+
} finally {
|
|
132
|
+
this.current = undefined;
|
|
133
|
+
this.onDidChangeEmitter.fire();
|
|
134
|
+
cts.dispose();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
protected notifyFailure(reason: string): void {
|
|
139
|
+
this.messageService.error(
|
|
140
|
+
nls.localize(
|
|
141
|
+
'theia/ai-ide/commit-message/failed',
|
|
142
|
+
'Failed to generate commit message: {0}',
|
|
143
|
+
reason
|
|
144
|
+
)
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Returns `true` if the operation should proceed. */
|
|
149
|
+
protected async confirmOverwrite(repository: ScmRepository): Promise<boolean> {
|
|
150
|
+
if (!repository.input.value.trim()) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
return !!await new ConfirmDialog({
|
|
154
|
+
title: nls.localize('theia/ai-ide/commit-message/replace-title', 'Replace Commit Message'),
|
|
155
|
+
msg: nls.localize(
|
|
156
|
+
'theia/ai-ide/commit-message/replace-prompt',
|
|
157
|
+
'The commit message field is not empty. Replace its content with the generated message?'
|
|
158
|
+
),
|
|
159
|
+
ok: nls.localizeByDefault('Replace'),
|
|
160
|
+
cancel: Dialog.CANCEL
|
|
161
|
+
}).open();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -95,7 +95,11 @@ import { ContextFilesVariableContribution } from '../common/context-files-variab
|
|
|
95
95
|
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
|
96
96
|
import { TemplatePreferenceContribution } from './template-preference-contribution';
|
|
97
97
|
import { ChatWelcomeMessageProvider } from '@theia/ai-chat-ui/lib/browser/chat-tree-view';
|
|
98
|
+
import { WalkthroughProvider } from '@theia/getting-started/lib/common/walkthrough-provider';
|
|
98
99
|
import { IdeChatWelcomeMessageProvider } from './ide-chat-welcome-message-provider';
|
|
100
|
+
import { AiGettingStartedWalkthroughProvider } from './ai-getting-started-walkthrough';
|
|
101
|
+
import { AiGettingStartedContribution } from './ai-getting-started-contribution';
|
|
102
|
+
import { AiWalkthroughContextKeys } from './ai-walkthrough-context-keys';
|
|
99
103
|
import { ChatSessionsWelcomeMessageProvider } from './chat-sessions-welcome-message-provider';
|
|
100
104
|
import { ChatSessionItemActionContribution, DefaultChatSessionItemActionContribution } from './chat-session-item-action-contribution';
|
|
101
105
|
import { AiAllowAllModeChatBanner } from './ai-allow-all-mode-chat-banner';
|
|
@@ -148,6 +152,12 @@ import { AIFirstPerspectiveContribution } from './ai-first-perspective-contribut
|
|
|
148
152
|
import { ChatSessionListService } from './chat-session-list-service';
|
|
149
153
|
import { AISessionsWidget } from './ai-sessions-widget';
|
|
150
154
|
import { AISessionsViewContribution } from './ai-sessions-view-contribution';
|
|
155
|
+
import { CommitMessageAgent } from './commit-message-agent';
|
|
156
|
+
import { CommitMessageRunner } from './commit-message-runner';
|
|
157
|
+
import { CommitMessageCommandContribution } from './commit-message-command-contribution';
|
|
158
|
+
import { GetGitChangesTool } from './git-changes-tool';
|
|
159
|
+
import { AiAwareScmCommitWidget } from './ai-aware-scm-commit-widget';
|
|
160
|
+
import { ScmCommitWidget } from '@theia/scm/lib/browser/scm-commit-widget';
|
|
151
161
|
|
|
152
162
|
export default new ContainerModule((bind, _unbind, _isBound, rebind) => {
|
|
153
163
|
bind(PreferenceContribution).toConstantValue({ schema: aiIdePreferenceSchema });
|
|
@@ -217,6 +227,11 @@ export default new ContainerModule((bind, _unbind, _isBound, rebind) => {
|
|
|
217
227
|
|
|
218
228
|
bind(ChatSessionListService).toSelf().inSingletonScope();
|
|
219
229
|
|
|
230
|
+
bind(WalkthroughProvider).to(AiGettingStartedWalkthroughProvider).inSingletonScope();
|
|
231
|
+
bind(CommandContribution).to(AiGettingStartedContribution).inSingletonScope();
|
|
232
|
+
bind(AiWalkthroughContextKeys).toSelf().inSingletonScope();
|
|
233
|
+
bind(FrontendApplicationContribution).toService(AiWalkthroughContextKeys);
|
|
234
|
+
|
|
220
235
|
bind(ChatWelcomeMessageProvider).to(IdeChatWelcomeMessageProvider).inSingletonScope();
|
|
221
236
|
bind(ChatWelcomeMessageProvider).to(ChatSessionsWelcomeMessageProvider).inSingletonScope();
|
|
222
237
|
bind(ChatBannerProvider).to(AiAllowAllModeChatBanner).inSingletonScope();
|
|
@@ -386,4 +401,16 @@ export default new ContainerModule((bind, _unbind, _isBound, rebind) => {
|
|
|
386
401
|
.inSingletonScope();
|
|
387
402
|
bindViewContribution(bind, AISessionsViewContribution);
|
|
388
403
|
bind(TabBarToolbarContribution).toService(AISessionsViewContribution);
|
|
404
|
+
|
|
405
|
+
// CommitMessageAgent is a plain (non-chat) `Agent`: it is driven exclusively from the SCM
|
|
406
|
+
// commit widget via `CommitMessageRunner`, so it does not appear in chat `@`-mention
|
|
407
|
+
// completion or the chat agent list. The `Agent` binding registers its prompt fragments
|
|
408
|
+
// with the prompt service.
|
|
409
|
+
bind(CommitMessageAgent).toSelf().inSingletonScope();
|
|
410
|
+
bind(Agent).toService(CommitMessageAgent);
|
|
411
|
+
bindToolProvider(GetGitChangesTool, bind);
|
|
412
|
+
bind(CommitMessageRunner).toSelf().inSingletonScope();
|
|
413
|
+
bind(CommitMessageCommandContribution).toSelf().inSingletonScope();
|
|
414
|
+
bind(CommandContribution).toService(CommitMessageCommandContribution);
|
|
415
|
+
rebind(ScmCommitWidget).to(AiAwareScmCommitWidget);
|
|
389
416
|
});
|
|
@@ -0,0 +1,200 @@
|
|
|
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 { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
|
18
|
+
let disableJSDOM = enableJSDOM();
|
|
19
|
+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
|
20
|
+
FrontendApplicationConfigProvider.set({});
|
|
21
|
+
|
|
22
|
+
import { expect } from 'chai';
|
|
23
|
+
import * as sinon from 'sinon';
|
|
24
|
+
import { Container } from '@theia/core/shared/inversify';
|
|
25
|
+
import URI from '@theia/core/lib/common/uri';
|
|
26
|
+
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
|
27
|
+
import { ScmRepository } from '@theia/scm/lib/browser/scm-repository';
|
|
28
|
+
import { ScmService } from '@theia/scm/lib/browser/scm-service';
|
|
29
|
+
import { ShellExecutionRequest, ShellExecutionResult, ShellExecutionServer } from '@theia/ai-terminal/lib/common/shell-execution-server';
|
|
30
|
+
import { GET_GIT_CHANGES_FUNCTION_ID, GetGitChangesTool, GitChangesRepositoryError } from './git-changes-tool';
|
|
31
|
+
|
|
32
|
+
disableJSDOM();
|
|
33
|
+
|
|
34
|
+
function shellResult(overrides: Partial<ShellExecutionResult> = {}): ShellExecutionResult {
|
|
35
|
+
return { success: true, exitCode: 0, stdout: 'staged diff', stderr: '', duration: 1, ...overrides };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeRepo(rootUri: string): ScmRepository {
|
|
39
|
+
return { provider: { rootUri } } as unknown as ScmRepository;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('GetGitChangesTool', () => {
|
|
43
|
+
|
|
44
|
+
let container: Container;
|
|
45
|
+
let shellServer: { execute: sinon.SinonStub; cancel: sinon.SinonStub };
|
|
46
|
+
let scmService: { repositories: ScmRepository[]; selectedRepository: ScmRepository | undefined };
|
|
47
|
+
let workspaceRoots: URI[];
|
|
48
|
+
let tool: GetGitChangesTool;
|
|
49
|
+
|
|
50
|
+
const backend = makeRepo('file:///work/backend');
|
|
51
|
+
const frontend = makeRepo('file:///work/frontend');
|
|
52
|
+
const nested = makeRepo('file:///work/backend/vendor/lib');
|
|
53
|
+
|
|
54
|
+
function cwdOfCall(index = 0): string | undefined {
|
|
55
|
+
return (shellServer.execute.getCall(index).args[0] as ShellExecutionRequest).cwd?.replace(/\\/g, '/');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
before(() => {
|
|
59
|
+
disableJSDOM = enableJSDOM();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
after(() => {
|
|
63
|
+
disableJSDOM();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
container = new Container();
|
|
68
|
+
shellServer = { execute: sinon.stub().resolves(shellResult()), cancel: sinon.stub().resolves(true) };
|
|
69
|
+
workspaceRoots = [new URI('file:///work/backend'), new URI('file:///work/frontend')];
|
|
70
|
+
scmService = { repositories: [backend, frontend, nested], selectedRepository: frontend };
|
|
71
|
+
const workspaceService: Partial<WorkspaceService> = {
|
|
72
|
+
tryGetRoots: () => workspaceRoots.map(resource => ({ resource } as never))
|
|
73
|
+
};
|
|
74
|
+
container.bind(ShellExecutionServer).toConstantValue(shellServer as unknown as ShellExecutionServer);
|
|
75
|
+
container.bind(ScmService).toConstantValue(scmService as unknown as ScmService);
|
|
76
|
+
container.bind(WorkspaceService).toConstantValue(workspaceService as WorkspaceService);
|
|
77
|
+
container.bind(GetGitChangesTool).toSelf();
|
|
78
|
+
tool = container.get(GetGitChangesTool);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterEach(() => sinon.restore());
|
|
82
|
+
|
|
83
|
+
it('exposes the well-known tool id getGitChanges with a repository parameter', () => {
|
|
84
|
+
const request = tool.getTool();
|
|
85
|
+
expect(request.id).to.equal(GET_GIT_CHANGES_FUNCTION_ID);
|
|
86
|
+
expect((request.parameters.properties.repository as { type: string }).type).to.equal('string');
|
|
87
|
+
expect(request.parameters.required ?? []).to.not.contain('repository');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('runs a cross-platform "git diff --cached" with no POSIX-only shell operators', async () => {
|
|
91
|
+
await tool.getTool().handler('{}');
|
|
92
|
+
const command = (shellServer.execute.firstCall.args[0] as ShellExecutionRequest).command;
|
|
93
|
+
expect(command).to.equal('git diff --cached --no-color');
|
|
94
|
+
expect(command).to.not.match(/;|\||xargs|sh -c|\/dev\/null/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('falls back to the selected repository when no repository argument is given', async () => {
|
|
98
|
+
await tool.getTool().handler('{}');
|
|
99
|
+
expect(cwdOfCall()).to.equal('/work/frontend');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('runs in the named repository rather than the selected one', async () => {
|
|
103
|
+
const result = await tool.getTool().handler(JSON.stringify({ repository: 'backend' })) as { output: string };
|
|
104
|
+
expect(result.output).to.equal('staged diff');
|
|
105
|
+
expect(cwdOfCall()).to.equal('/work/backend');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('identifies a repository nested inside a workspace root by its relative path', async () => {
|
|
109
|
+
await tool.getTool().handler(JSON.stringify({ repository: 'backend/vendor/lib' }));
|
|
110
|
+
expect(cwdOfCall()).to.equal('/work/backend/vendor/lib');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('labels the argument with the repository for the tool-call summary', () => {
|
|
114
|
+
const label = tool.getTool().getArgumentsShortLabel!(JSON.stringify({ repository: 'backend' }));
|
|
115
|
+
expect(label).to.deep.equal({ label: 'backend', hasMore: false });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('accepts an absolute path and ignores separator and case differences', async () => {
|
|
119
|
+
await tool.getTool().handler(JSON.stringify({ repository: '/work/backend' }));
|
|
120
|
+
expect(cwdOfCall()).to.equal('/work/backend');
|
|
121
|
+
|
|
122
|
+
await tool.getTool().handler(JSON.stringify({ repository: 'BackEnd/' }));
|
|
123
|
+
expect(cwdOfCall(1)).to.equal('/work/backend');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('reports the known repositories instead of running an unknown one', async () => {
|
|
127
|
+
const result = await tool.getTool().handler(JSON.stringify({ repository: '../../etc' })) as GitChangesRepositoryError;
|
|
128
|
+
expect(result.error).to.contain('../../etc');
|
|
129
|
+
expect(result.availableRepositories).to.deep.equal(['backend', 'frontend', 'backend/vendor/lib']);
|
|
130
|
+
expect(shellServer.execute.called).to.be.false;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('reports an ambiguous folder name instead of picking the first matching repository', async () => {
|
|
134
|
+
const otherLib = makeRepo('file:///work/frontend/vendor/lib');
|
|
135
|
+
scmService.repositories = [backend, frontend, nested, otherLib];
|
|
136
|
+
|
|
137
|
+
const result = await tool.getTool().handler(JSON.stringify({ repository: 'lib' })) as GitChangesRepositoryError;
|
|
138
|
+
expect(result.error).to.contain('ambiguous');
|
|
139
|
+
expect(shellServer.execute.called).to.be.false;
|
|
140
|
+
|
|
141
|
+
await tool.getTool().handler(JSON.stringify({ repository: 'frontend/vendor/lib' }));
|
|
142
|
+
expect(cwdOfCall()).to.equal('/work/frontend/vendor/lib');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('prefers an exact label match over a folder name shared with a nested repository', async () => {
|
|
146
|
+
const nestedBackend = makeRepo('file:///work/frontend/backend');
|
|
147
|
+
scmService.repositories = [backend, frontend, nestedBackend];
|
|
148
|
+
|
|
149
|
+
await tool.getTool().handler(JSON.stringify({ repository: 'backend' }));
|
|
150
|
+
expect(cwdOfCall()).to.equal('/work/backend');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('never derives the working directory from the argument, only from the matched repository', async () => {
|
|
154
|
+
await tool.getTool().handler(JSON.stringify({ repository: 'backend; rm -rf /' }));
|
|
155
|
+
expect(shellServer.execute.called).to.be.false;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('asks for a repository when none is named and none is selected', async () => {
|
|
159
|
+
scmService.selectedRepository = undefined;
|
|
160
|
+
const result = await tool.getTool().handler('{}') as GitChangesRepositoryError;
|
|
161
|
+
expect(result.error).to.contain('repository argument');
|
|
162
|
+
expect(shellServer.execute.called).to.be.false;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('throws instead of returning the error output when the git command fails', async () => {
|
|
166
|
+
shellServer.execute.resolves(shellResult({
|
|
167
|
+
success: false,
|
|
168
|
+
exitCode: 128,
|
|
169
|
+
stdout: '',
|
|
170
|
+
stderr: 'fatal: not a git repository'
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
let caught: Error | undefined;
|
|
174
|
+
try {
|
|
175
|
+
await tool.getStagedChanges(backend);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
caught = error as Error;
|
|
178
|
+
}
|
|
179
|
+
expect(caught?.message).to.match(/fatal: not a git repository/);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('returns an empty diff when the execution was canceled', async () => {
|
|
183
|
+
shellServer.execute.resolves(shellResult({ canceled: true, stdout: 'partial' }));
|
|
184
|
+
expect(await tool.getStagedChanges(backend)).to.equal('');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('keeps far more diff lines than the default shellExecute budget', async () => {
|
|
188
|
+
const stdout = Array.from({ length: 600 }, (_unused, index) => `+line ${index}`).join('\n');
|
|
189
|
+
shellServer.execute.resolves(shellResult({ stdout }));
|
|
190
|
+
|
|
191
|
+
const diff = await tool.getStagedChanges(backend);
|
|
192
|
+
|
|
193
|
+
expect(diff).to.contain('+line 300');
|
|
194
|
+
expect(diff).to.not.contain('lines omitted');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('does not declare checkAutoAction so confirmation flows through the normal ToolConfirmationManager', () => {
|
|
198
|
+
expect(tool.getTool().checkAutoAction).to.be.undefined;
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -0,0 +1,239 @@
|
|
|
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 { CancellationToken } from '@theia/core';
|
|
18
|
+
import URI from '@theia/core/lib/common/uri';
|
|
19
|
+
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
20
|
+
import { ToolInvocationContext, ToolRequest, ToolRequestParameters } from '@theia/ai-core';
|
|
21
|
+
import { ScmRepository } from '@theia/scm/lib/browser/scm-repository';
|
|
22
|
+
import { ScmService } from '@theia/scm/lib/browser/scm-service';
|
|
23
|
+
import { PredefinedShellTool } from '@theia/ai-terminal/lib/browser/predefined-shell-tool';
|
|
24
|
+
import {
|
|
25
|
+
OutputTruncationOptions,
|
|
26
|
+
ShellExecutionCanceledResult,
|
|
27
|
+
ShellExecutionToolResult
|
|
28
|
+
} from '@theia/ai-terminal/lib/common/shell-execution-server';
|
|
29
|
+
|
|
30
|
+
export const GET_GIT_CHANGES_FUNCTION_ID = 'getGitChanges';
|
|
31
|
+
|
|
32
|
+
/** Returned instead of a diff when the arguments do not identify a known repository. */
|
|
33
|
+
export interface GitChangesRepositoryError {
|
|
34
|
+
error: string;
|
|
35
|
+
availableRepositories: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export namespace GitChangesRepositoryError {
|
|
39
|
+
export function is(result: unknown): result is GitChangesRepositoryError {
|
|
40
|
+
return !!result && typeof result === 'object' && 'availableRepositories' in result;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns `git diff --cached` for one of the workspace's git repositories.
|
|
46
|
+
*
|
|
47
|
+
* `git diff --cached` is a plain, operator-free command that behaves the same on POSIX shells and
|
|
48
|
+
* Windows `cmd.exe`, and it renders staged additions of new files as complete addition diffs, so
|
|
49
|
+
* untracked files need no separate handling.
|
|
50
|
+
*
|
|
51
|
+
* The `repository` argument is matched against the repositories the SCM service actually knows
|
|
52
|
+
* about, and the command then runs in *that* repository's root. The argument never reaches the
|
|
53
|
+
* shell, so a repository name cannot inject a command. An unknown or ambiguous name is answered
|
|
54
|
+
* with the list of valid ones rather than a silent fallback, so the model cannot mistake one
|
|
55
|
+
* repository's diff for another's.
|
|
56
|
+
*/
|
|
57
|
+
@injectable()
|
|
58
|
+
export class GetGitChangesTool extends PredefinedShellTool {
|
|
59
|
+
|
|
60
|
+
@inject(ScmService)
|
|
61
|
+
protected readonly scmService: ScmService;
|
|
62
|
+
|
|
63
|
+
readonly id = GET_GIT_CHANGES_FUNCTION_ID;
|
|
64
|
+
|
|
65
|
+
readonly description =
|
|
66
|
+
'Returns the staged changes (`git diff --cached`) of a git repository in the workspace, ' +
|
|
67
|
+
'including the full content of newly added files. Unstaged and untracked files are not ' +
|
|
68
|
+
'included. Returns an empty diff when nothing is staged, and an error listing the known ' +
|
|
69
|
+
'repositories when the requested one does not exist.';
|
|
70
|
+
|
|
71
|
+
protected override readonly parameters: ToolRequestParameters = {
|
|
72
|
+
type: 'object',
|
|
73
|
+
properties: {
|
|
74
|
+
repository: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
description:
|
|
77
|
+
'Which repository to read. Use the path of the repository root relative to the ' +
|
|
78
|
+
'workspace (e.g. "backend" or "backend/tools"), or its absolute path. Omit it to ' +
|
|
79
|
+
'use the repository currently selected in the Source Control view; in a ' +
|
|
80
|
+
'single-repository workspace that is always the right one, but in a workspace ' +
|
|
81
|
+
'with several repositories pass the name explicitly so the diff is not taken ' +
|
|
82
|
+
'from whichever repository the user happens to have selected.'
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A diff is only useful as a whole, so the `shellExecute` budget of 50 head plus 50 tail
|
|
89
|
+
* lines is far too tight. The head is favoured over the tail because the leading hunks
|
|
90
|
+
* describe the change; the backend caps stdout at 1MB regardless.
|
|
91
|
+
*/
|
|
92
|
+
protected override readonly truncation: OutputTruncationOptions = {
|
|
93
|
+
headLines: 2000,
|
|
94
|
+
tailLines: 500,
|
|
95
|
+
maxLineLength: 2000
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
override getTool(): ToolRequest {
|
|
99
|
+
return {
|
|
100
|
+
...super.getTool(),
|
|
101
|
+
// The base class runs the command unconditionally; an unknown repository has to be
|
|
102
|
+
// answered before that, so the handler is wrapped rather than `execute` overridden.
|
|
103
|
+
handler: (argString: string, ctx?: ToolInvocationContext) => this.handle(argString, ctx),
|
|
104
|
+
getArgumentsShortLabel: (args: string) => {
|
|
105
|
+
try {
|
|
106
|
+
const repository = JSON.parse(args)?.repository;
|
|
107
|
+
return typeof repository === 'string' && repository ? { label: repository, hasMore: false } : undefined;
|
|
108
|
+
} catch {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Runs the diff for the given repository and returns it. Used directly by the commit-message
|
|
117
|
+
* generator, which already knows its repository and must not go through tool confirmation.
|
|
118
|
+
*
|
|
119
|
+
* Returns an empty string when the execution was canceled.
|
|
120
|
+
*
|
|
121
|
+
* @throws if the git command fails, so a `fatal: ...` message is never mistaken for a diff.
|
|
122
|
+
*/
|
|
123
|
+
async getStagedChanges(repository: ScmRepository, cancellationToken?: CancellationToken): Promise<string> {
|
|
124
|
+
const result = await this.runShellCommand({
|
|
125
|
+
command: this.buildCommand(),
|
|
126
|
+
cwd: new URI(repository.provider.rootUri).path.fsPath(),
|
|
127
|
+
timeout: this.timeout,
|
|
128
|
+
ctx: { cancellationToken },
|
|
129
|
+
truncation: this.truncation
|
|
130
|
+
});
|
|
131
|
+
if (ShellExecutionCanceledResult.is(result)) {
|
|
132
|
+
return '';
|
|
133
|
+
}
|
|
134
|
+
if (!result.success) {
|
|
135
|
+
const detail = result.error || result.output || `exit code ${result.exitCode}`;
|
|
136
|
+
throw new Error(`Failed to read the staged changes: ${detail}`);
|
|
137
|
+
}
|
|
138
|
+
return result.output;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
protected buildCommand(): string {
|
|
142
|
+
return 'git diff --cached --no-color';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
protected async handle(
|
|
146
|
+
argString: string,
|
|
147
|
+
ctx?: ToolInvocationContext
|
|
148
|
+
): Promise<ShellExecutionToolResult | ShellExecutionCanceledResult | GitChangesRepositoryError> {
|
|
149
|
+
const args: Record<string, unknown> = argString ? JSON.parse(argString) : {};
|
|
150
|
+
const requested = this.requestedRepository(args);
|
|
151
|
+
const candidates = this.findRepositories(args);
|
|
152
|
+
if (candidates.length > 1) {
|
|
153
|
+
return {
|
|
154
|
+
error: `Repository '${requested}' is ambiguous. Pass the relative path shown in availableRepositories or the absolute path of the repository root.`,
|
|
155
|
+
availableRepositories: this.availableRepositories()
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (candidates.length === 0) {
|
|
159
|
+
return {
|
|
160
|
+
error: requested
|
|
161
|
+
? `Unknown repository '${requested}'.`
|
|
162
|
+
: 'No repository is selected in the Source Control view. Pass the repository argument.',
|
|
163
|
+
availableRepositories: this.availableRepositories()
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return this.execute(argString, ctx);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
protected resolveWorkspaceRoot(args: Record<string, unknown>): string {
|
|
170
|
+
const repository = this.findRepository(args);
|
|
171
|
+
if (!repository) {
|
|
172
|
+
// Guarded by `handle`; failing loudly here keeps an unresolved repository from
|
|
173
|
+
// silently running the diff in the backend process' own working directory.
|
|
174
|
+
throw new Error(`Unknown repository '${this.requestedRepository(args) ?? ''}'.`);
|
|
175
|
+
}
|
|
176
|
+
return new URI(repository.provider.rootUri).path.fsPath();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
protected requestedRepository(args: Record<string, unknown>): string | undefined {
|
|
180
|
+
return typeof args.repository === 'string' && args.repository.trim() ? args.repository.trim() : undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The repository identified by the `repository` argument, or `undefined` if it is unknown or ambiguous. */
|
|
184
|
+
protected findRepository(args: Record<string, unknown>): ScmRepository | undefined {
|
|
185
|
+
const candidates = this.findRepositories(args);
|
|
186
|
+
return candidates.length === 1 ? candidates[0] : undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Matches the `repository` argument against the known repositories by label or path. The
|
|
191
|
+
* folder name alone is only used when nothing matches by label or path. Falls back to the
|
|
192
|
+
* selected repository when the argument is absent. More than one result means the argument
|
|
193
|
+
* is ambiguous.
|
|
194
|
+
*/
|
|
195
|
+
protected findRepositories(args: Record<string, unknown>): ScmRepository[] {
|
|
196
|
+
const requested = this.requestedRepository(args);
|
|
197
|
+
if (!requested) {
|
|
198
|
+
return this.scmService.selectedRepository ? [this.scmService.selectedRepository] : [];
|
|
199
|
+
}
|
|
200
|
+
const normalized = this.normalize(requested);
|
|
201
|
+
const repositories = this.scmService.repositories;
|
|
202
|
+
const byLabelOrPath = repositories.filter(repository => {
|
|
203
|
+
const root = new URI(repository.provider.rootUri);
|
|
204
|
+
return this.normalize(this.repositoryLabel(repository)) === normalized
|
|
205
|
+
|| this.normalize(root.path.fsPath()) === normalized
|
|
206
|
+
|| this.normalize(root.toString()) === normalized;
|
|
207
|
+
});
|
|
208
|
+
if (byLabelOrPath.length > 0) {
|
|
209
|
+
return byLabelOrPath;
|
|
210
|
+
}
|
|
211
|
+
return repositories.filter(repository => this.normalize(new URI(repository.provider.rootUri).path.base) === normalized);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
protected availableRepositories(): string[] {
|
|
215
|
+
return this.scmService.repositories.map(repository => this.repositoryLabel(repository));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Identifies a repository by the path of its root relative to the workspace, so that the label
|
|
220
|
+
* is usable as the `repository` argument of a follow-up call. Workspace roots sharing a folder
|
|
221
|
+
* name produce identical labels, which {@link findRepositories} reports as ambiguous.
|
|
222
|
+
*/
|
|
223
|
+
protected repositoryLabel(repository: ScmRepository): string {
|
|
224
|
+
const root = new URI(repository.provider.rootUri);
|
|
225
|
+
for (const workspaceRoot of this.workspaceService.tryGetRoots()) {
|
|
226
|
+
if (!workspaceRoot.resource.isEqualOrParent(root)) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const relative = workspaceRoot.resource.relative(root)?.toString();
|
|
230
|
+
return relative ? `${workspaceRoot.resource.path.base}/${relative}` : workspaceRoot.resource.path.base;
|
|
231
|
+
}
|
|
232
|
+
return root.path.base;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Path separators and case differ across platforms and between user phrasings. */
|
|
236
|
+
protected normalize(value: string): string {
|
|
237
|
+
return value.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
|
|
238
|
+
}
|
|
239
|
+
}
|