@theia/ai-ide 1.76.0-next.7 → 1.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/lib/browser/ai-allow-all-mode-chat-banner.js +1 -1
  2. package/lib/browser/ai-allow-all-mode-chat-banner.js.map +1 -1
  3. package/lib/browser/ai-aware-scm-commit-widget.d.ts +27 -0
  4. package/lib/browser/ai-aware-scm-commit-widget.d.ts.map +1 -0
  5. package/lib/browser/ai-aware-scm-commit-widget.js +142 -0
  6. package/lib/browser/ai-aware-scm-commit-widget.js.map +1 -0
  7. package/lib/browser/ai-configuration/ai-configuration-detail-widget.d.ts +10 -4
  8. package/lib/browser/ai-configuration/ai-configuration-detail-widget.d.ts.map +1 -1
  9. package/lib/browser/ai-configuration/ai-configuration-detail-widget.js +12 -7
  10. package/lib/browser/ai-configuration/ai-configuration-detail-widget.js.map +1 -1
  11. package/lib/browser/ai-configuration/ai-configuration-detail-widget.spec.js +14 -14
  12. package/lib/browser/ai-configuration/ai-configuration-detail-widget.spec.js.map +1 -1
  13. package/lib/browser/ai-configuration/ai-configuration-tree-widget.d.ts +6 -0
  14. package/lib/browser/ai-configuration/ai-configuration-tree-widget.d.ts.map +1 -1
  15. package/lib/browser/ai-configuration/ai-configuration-tree-widget.js +9 -0
  16. package/lib/browser/ai-configuration/ai-configuration-tree-widget.js.map +1 -1
  17. package/lib/browser/ai-configuration/categories/agent-capabilities-settings.d.ts.map +1 -1
  18. package/lib/browser/ai-configuration/categories/agent-capabilities-settings.js +2 -1
  19. package/lib/browser/ai-configuration/categories/agent-capabilities-settings.js.map +1 -1
  20. package/lib/browser/ai-configuration/categories/agent-chips.js +1 -1
  21. package/lib/browser/ai-configuration/categories/agent-chips.js.map +1 -1
  22. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.d.ts +8 -0
  23. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.d.ts.map +1 -1
  24. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.js +27 -8
  25. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.js.map +1 -1
  26. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.spec.js +38 -2
  27. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.spec.js.map +1 -1
  28. package/lib/browser/ai-configuration/categories/models-configuration-category.d.ts +13 -2
  29. package/lib/browser/ai-configuration/categories/models-configuration-category.d.ts.map +1 -1
  30. package/lib/browser/ai-configuration/categories/models-configuration-category.js +41 -2
  31. package/lib/browser/ai-configuration/categories/models-configuration-category.js.map +1 -1
  32. package/lib/browser/ai-configuration/categories/models-configuration-category.spec.js +32 -2
  33. package/lib/browser/ai-configuration/categories/models-configuration-category.spec.js.map +1 -1
  34. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.d.ts +26 -0
  35. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.d.ts.map +1 -0
  36. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.js +212 -0
  37. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.js.map +1 -0
  38. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.d.ts +2 -0
  39. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.d.ts.map +1 -0
  40. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.js +423 -0
  41. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.js.map +1 -0
  42. package/lib/browser/ai-configuration/language-model-renderer.d.ts.map +1 -1
  43. package/lib/browser/ai-configuration/language-model-renderer.js +10 -3
  44. package/lib/browser/ai-configuration/language-model-renderer.js.map +1 -1
  45. package/lib/browser/ai-getting-started-commands.d.ts +10 -0
  46. package/lib/browser/ai-getting-started-commands.d.ts.map +1 -0
  47. package/lib/browser/ai-getting-started-commands.js +34 -0
  48. package/lib/browser/ai-getting-started-commands.js.map +1 -0
  49. package/lib/browser/ai-getting-started-contribution.d.ts +20 -0
  50. package/lib/browser/ai-getting-started-contribution.d.ts.map +1 -0
  51. package/lib/browser/ai-getting-started-contribution.js +70 -0
  52. package/lib/browser/ai-getting-started-contribution.js.map +1 -0
  53. package/lib/browser/ai-getting-started-walkthrough.d.ts +41 -0
  54. package/lib/browser/ai-getting-started-walkthrough.d.ts.map +1 -0
  55. package/lib/browser/ai-getting-started-walkthrough.js +225 -0
  56. package/lib/browser/ai-getting-started-walkthrough.js.map +1 -0
  57. package/lib/browser/ai-getting-started-walkthrough.spec.d.ts +2 -0
  58. package/lib/browser/ai-getting-started-walkthrough.spec.d.ts.map +1 -0
  59. package/lib/browser/ai-getting-started-walkthrough.spec.js +124 -0
  60. package/lib/browser/ai-getting-started-walkthrough.spec.js.map +1 -0
  61. package/lib/browser/ai-walkthrough-context-keys.d.ts +55 -0
  62. package/lib/browser/ai-walkthrough-context-keys.d.ts.map +1 -0
  63. package/lib/browser/ai-walkthrough-context-keys.js +145 -0
  64. package/lib/browser/ai-walkthrough-context-keys.js.map +1 -0
  65. package/lib/browser/ai-walkthrough-context-keys.spec.d.ts +2 -0
  66. package/lib/browser/ai-walkthrough-context-keys.spec.d.ts.map +1 -0
  67. package/lib/browser/ai-walkthrough-context-keys.spec.js +166 -0
  68. package/lib/browser/ai-walkthrough-context-keys.spec.js.map +1 -0
  69. package/lib/browser/chat-session-item.js +2 -2
  70. package/lib/browser/chat-session-item.js.map +1 -1
  71. package/lib/browser/commit-message-agent.d.ts +31 -0
  72. package/lib/browser/commit-message-agent.d.ts.map +1 -0
  73. package/lib/browser/commit-message-agent.js +99 -0
  74. package/lib/browser/commit-message-agent.js.map +1 -0
  75. package/lib/browser/commit-message-agent.spec.d.ts +2 -0
  76. package/lib/browser/commit-message-agent.spec.d.ts.map +1 -0
  77. package/lib/browser/commit-message-agent.spec.js +82 -0
  78. package/lib/browser/commit-message-agent.spec.js.map +1 -0
  79. package/lib/browser/commit-message-command-contribution.d.ts +15 -0
  80. package/lib/browser/commit-message-command-contribution.d.ts.map +1 -0
  81. package/lib/browser/commit-message-command-contribution.js +73 -0
  82. package/lib/browser/commit-message-command-contribution.js.map +1 -0
  83. package/lib/browser/commit-message-commands.d.ts +5 -0
  84. package/lib/browser/commit-message-commands.d.ts.map +1 -0
  85. package/lib/browser/commit-message-commands.js +28 -0
  86. package/lib/browser/commit-message-commands.js.map +1 -0
  87. package/lib/browser/commit-message-prompt-template.d.ts +11 -0
  88. package/lib/browser/commit-message-prompt-template.d.ts.map +1 -0
  89. package/lib/browser/commit-message-prompt-template.js +63 -0
  90. package/lib/browser/commit-message-prompt-template.js.map +1 -0
  91. package/lib/browser/commit-message-runner.d.ts +41 -0
  92. package/lib/browser/commit-message-runner.d.ts.map +1 -0
  93. package/lib/browser/commit-message-runner.js +143 -0
  94. package/lib/browser/commit-message-runner.js.map +1 -0
  95. package/lib/browser/commit-message-runner.spec.d.ts +2 -0
  96. package/lib/browser/commit-message-runner.spec.d.ts.map +1 -0
  97. package/lib/browser/commit-message-runner.spec.js +206 -0
  98. package/lib/browser/commit-message-runner.spec.js.map +1 -0
  99. package/lib/browser/frontend-module.d.ts.map +1 -1
  100. package/lib/browser/frontend-module.js +25 -0
  101. package/lib/browser/frontend-module.js.map +1 -1
  102. package/lib/browser/git-changes-tool.d.ts +73 -0
  103. package/lib/browser/git-changes-tool.d.ts.map +1 -0
  104. package/lib/browser/git-changes-tool.js +217 -0
  105. package/lib/browser/git-changes-tool.js.map +1 -0
  106. package/lib/browser/git-changes-tool.spec.d.ts +2 -0
  107. package/lib/browser/git-changes-tool.spec.d.ts.map +1 -0
  108. package/lib/browser/git-changes-tool.spec.js +167 -0
  109. package/lib/browser/git-changes-tool.spec.js.map +1 -0
  110. package/lib/browser/ide-chat-welcome-message-provider.d.ts +26 -28
  111. package/lib/browser/ide-chat-welcome-message-provider.d.ts.map +1 -1
  112. package/lib/browser/ide-chat-welcome-message-provider.js +61 -151
  113. package/lib/browser/ide-chat-welcome-message-provider.js.map +1 -1
  114. package/lib/browser/review/pr-review-capability-contribution.js +1 -1
  115. package/lib/browser/review/pr-review-capability-contribution.js.map +1 -1
  116. package/lib/browser/user-interaction-tool-renderer.d.ts +6 -0
  117. package/lib/browser/user-interaction-tool-renderer.d.ts.map +1 -1
  118. package/lib/browser/user-interaction-tool-renderer.js +36 -7
  119. package/lib/browser/user-interaction-tool-renderer.js.map +1 -1
  120. package/lib/browser/user-interaction-tool.d.ts +23 -1
  121. package/lib/browser/user-interaction-tool.d.ts.map +1 -1
  122. package/lib/browser/user-interaction-tool.js +56 -17
  123. package/lib/browser/user-interaction-tool.js.map +1 -1
  124. package/lib/browser/user-interaction-tool.spec.js +151 -2
  125. package/lib/browser/user-interaction-tool.spec.js.map +1 -1
  126. package/lib/common/command-prompt-template.js +1 -1
  127. package/lib/common/user-interaction-tool.d.ts +19 -1
  128. package/lib/common/user-interaction-tool.d.ts.map +1 -1
  129. package/lib/common/user-interaction-tool.js +100 -29
  130. package/lib/common/user-interaction-tool.js.map +1 -1
  131. package/lib/common/user-interaction-tool.spec.js +215 -174
  132. package/lib/common/user-interaction-tool.spec.js.map +1 -1
  133. package/package.json +26 -25
  134. package/src/browser/ai-allow-all-mode-chat-banner.tsx +2 -2
  135. package/src/browser/ai-aware-scm-commit-widget.tsx +153 -0
  136. package/src/browser/ai-configuration/ai-configuration-detail-widget.spec.ts +14 -14
  137. package/src/browser/ai-configuration/ai-configuration-detail-widget.tsx +12 -7
  138. package/src/browser/ai-configuration/ai-configuration-tree-widget.tsx +8 -0
  139. package/src/browser/ai-configuration/categories/agent-capabilities-settings.tsx +7 -8
  140. package/src/browser/ai-configuration/categories/agent-chips.tsx +1 -1
  141. package/src/browser/ai-configuration/categories/model-aliases-configuration-category.spec.ts +40 -2
  142. package/src/browser/ai-configuration/categories/model-aliases-configuration-category.tsx +31 -10
  143. package/src/browser/ai-configuration/categories/models-configuration-category.spec.ts +47 -2
  144. package/src/browser/ai-configuration/categories/models-configuration-category.tsx +52 -5
  145. package/src/browser/ai-configuration/categories/provider-model-discovery-section.spec.tsx +451 -0
  146. package/src/browser/ai-configuration/categories/provider-model-discovery-section.tsx +341 -0
  147. package/src/browser/ai-configuration/language-model-renderer.tsx +11 -3
  148. package/src/browser/ai-getting-started-commands.ts +33 -0
  149. package/src/browser/ai-getting-started-contribution.ts +64 -0
  150. package/src/browser/ai-getting-started-walkthrough.spec.ts +146 -0
  151. package/src/browser/ai-getting-started-walkthrough.ts +249 -0
  152. package/src/browser/ai-walkthrough-context-keys.spec.ts +209 -0
  153. package/src/browser/ai-walkthrough-context-keys.ts +147 -0
  154. package/src/browser/chat-session-item.tsx +2 -2
  155. package/src/browser/commit-message-agent.spec.ts +97 -0
  156. package/src/browser/commit-message-agent.ts +113 -0
  157. package/src/browser/commit-message-command-contribution.ts +67 -0
  158. package/src/browser/commit-message-commands.ts +25 -0
  159. package/src/browser/commit-message-prompt-template.ts +65 -0
  160. package/src/browser/commit-message-runner.spec.ts +266 -0
  161. package/src/browser/commit-message-runner.ts +163 -0
  162. package/src/browser/frontend-module.ts +27 -0
  163. package/src/browser/git-changes-tool.spec.ts +200 -0
  164. package/src/browser/git-changes-tool.ts +239 -0
  165. package/src/browser/ide-chat-welcome-message-provider.tsx +93 -230
  166. package/src/browser/review/pr-review-capability-contribution.ts +1 -1
  167. package/src/browser/style/commit-message-overlay.css +64 -0
  168. package/src/browser/style/index.css +4 -42
  169. package/src/browser/user-interaction-tool-renderer.tsx +38 -8
  170. package/src/browser/user-interaction-tool.spec.ts +201 -2
  171. package/src/browser/user-interaction-tool.ts +69 -18
  172. package/src/common/command-prompt-template.ts +1 -1
  173. package/src/common/user-interaction-tool.spec.ts +217 -161
  174. package/src/common/user-interaction-tool.ts +127 -32
@@ -0,0 +1,97 @@
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 { expect } from 'chai';
18
+ import * as sinon from 'sinon';
19
+ import { Container } from '@theia/core/shared/inversify';
20
+ import { LanguageModel, LanguageModelRegistry, LanguageModelService, PromptService, UserRequest } from '@theia/ai-core';
21
+ import { CommitMessageAgent } from './commit-message-agent';
22
+ import { COMMIT_MESSAGE_SYSTEM_PROMPT_ID, COMMIT_MESSAGE_USER_PROMPT_ID } from './commit-message-prompt-template';
23
+
24
+ describe('CommitMessageAgent', () => {
25
+
26
+ let container: Container;
27
+ let agent: CommitMessageAgent;
28
+ let registry: { selectLanguageModel: sinon.SinonStub };
29
+ let promptService: { getResolvedPromptFragmentWithoutFunctions: sinon.SinonStub; getPromptVariantInfo: sinon.SinonStub };
30
+ let languageModelService: { sendRequest: sinon.SinonStub };
31
+ const fakeModel = {} as LanguageModel;
32
+
33
+ beforeEach(() => {
34
+ container = new Container();
35
+ registry = { selectLanguageModel: sinon.stub().resolves(fakeModel) };
36
+ promptService = {
37
+ getResolvedPromptFragmentWithoutFunctions: sinon.stub().callsFake((id: string) => Promise.resolve({ text: `resolved:${id}` })),
38
+ getPromptVariantInfo: sinon.stub().returns(undefined)
39
+ };
40
+ languageModelService = { sendRequest: sinon.stub().resolves({ text: 'feat: add thing\n' }) };
41
+
42
+ container.bind(LanguageModelRegistry).toConstantValue(registry as unknown as LanguageModelRegistry);
43
+ container.bind(PromptService).toConstantValue(promptService as unknown as PromptService);
44
+ container.bind(LanguageModelService).toConstantValue(languageModelService as unknown as LanguageModelService);
45
+ container.bind(CommitMessageAgent).toSelf();
46
+
47
+ agent = container.get(CommitMessageAgent);
48
+ });
49
+
50
+ afterEach(() => sinon.restore());
51
+
52
+ it('is a plain agent that exposes no functions', () => {
53
+ expect(agent.functions).to.be.empty;
54
+ });
55
+
56
+ it('resolves both prompts, sends the request and returns the trimmed message', async () => {
57
+ const result = await agent.generateCommitMessage('the diff');
58
+
59
+ expect(result).to.equal('feat: add thing');
60
+ expect(promptService.getResolvedPromptFragmentWithoutFunctions.calledWith(COMMIT_MESSAGE_SYSTEM_PROMPT_ID)).to.be.true;
61
+ expect(promptService.getResolvedPromptFragmentWithoutFunctions.calledWith(COMMIT_MESSAGE_USER_PROMPT_ID)).to.be.true;
62
+ expect(languageModelService.sendRequest.calledOnce).to.be.true;
63
+ });
64
+
65
+ it('injects the diff into the prompt parameters', async () => {
66
+ await agent.generateCommitMessage('the diff');
67
+
68
+ const [, params] = promptService.getResolvedPromptFragmentWithoutFunctions.firstCall.args;
69
+ expect(params).to.deep.equal({ changes: 'the diff' });
70
+ });
71
+
72
+ it('sends a system and a user message tagged with the agent id and the cancellation token', async () => {
73
+ const token = { isCancellationRequested: false } as unknown as UserRequest['cancellationToken'];
74
+ await agent.generateCommitMessage('the diff', token);
75
+
76
+ const request = languageModelService.sendRequest.firstCall.args[1] as UserRequest;
77
+ expect(request.agentId).to.equal(agent.id);
78
+ expect(request.cancellationToken).to.equal(token);
79
+ expect(request.messages.map(m => m.actor)).to.deep.equal(['system', 'user']);
80
+ });
81
+
82
+ it('throws when no language model is available', async () => {
83
+ registry.selectLanguageModel.resolves(undefined);
84
+
85
+ const error = await agent.generateCommitMessage('the diff').then(() => undefined, e => e);
86
+ expect(error).to.be.instanceOf(Error);
87
+ expect((error as Error).message).to.match(/No language model/);
88
+ });
89
+
90
+ it('throws when the prompts cannot be resolved', async () => {
91
+ promptService.getResolvedPromptFragmentWithoutFunctions.resolves(undefined);
92
+
93
+ const error = await agent.generateCommitMessage('the diff').then(() => undefined, e => e);
94
+ expect(error).to.be.instanceOf(Error);
95
+ expect((error as Error).message).to.match(/prompt service/);
96
+ });
97
+ });
@@ -0,0 +1,113 @@
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 {
18
+ Agent,
19
+ getTextOfResponse,
20
+ LanguageModelRegistry,
21
+ LanguageModelRequirement,
22
+ LanguageModelService,
23
+ PromptService,
24
+ UserRequest
25
+ } from '@theia/ai-core';
26
+ import { generateUuid, nls } from '@theia/core';
27
+ import { CancellationToken } from '@theia/core/lib/common/cancellation';
28
+ import { inject, injectable } from '@theia/core/shared/inversify';
29
+ import { commitMessagePrompts, COMMIT_MESSAGE_SYSTEM_PROMPT_ID, COMMIT_MESSAGE_USER_PROMPT_ID } from './commit-message-prompt-template';
30
+
31
+ export const COMMIT_MESSAGE_AGENT_ID = 'Commit Message';
32
+
33
+ @injectable()
34
+ export class CommitMessageAgent implements Agent {
35
+
36
+ id = COMMIT_MESSAGE_AGENT_ID;
37
+ name = COMMIT_MESSAGE_AGENT_ID;
38
+
39
+ description = nls.localize('theia/ai-ide/commitMessageAgent/description',
40
+ 'Generates git commit messages from the staged changes in the workspace repository. ' +
41
+ 'Invoked from the SCM commit-message input via the AI sparkle button.');
42
+
43
+ variables = [];
44
+ functions = [];
45
+
46
+ prompts = commitMessagePrompts;
47
+
48
+ languageModelRequirements: LanguageModelRequirement[] = [{
49
+ purpose: 'commit-message',
50
+ identifier: 'default/universal',
51
+ }];
52
+
53
+ agentSpecificVariables = [
54
+ {
55
+ name: 'changes',
56
+ usedInPrompt: true,
57
+ description: nls.localize(
58
+ 'theia/ai-ide/commitMessageAgent/vars/changes/description',
59
+ 'The unified git diff of the staged changes the commit message is generated from.'
60
+ )
61
+ }
62
+ ];
63
+
64
+ @inject(LanguageModelRegistry)
65
+ protected readonly languageModelRegistry: LanguageModelRegistry;
66
+
67
+ @inject(PromptService)
68
+ protected readonly promptService: PromptService;
69
+
70
+ @inject(LanguageModelService)
71
+ protected readonly languageModelService: LanguageModelService;
72
+
73
+ /**
74
+ * Generates a commit message from the given unified diff. Returns the trimmed message text.
75
+ *
76
+ * @throws if no language model is available or the prompts cannot be resolved.
77
+ */
78
+ async generateCommitMessage(changes: string, cancellationToken?: CancellationToken): Promise<string> {
79
+ const lm = await this.languageModelRegistry.selectLanguageModel({
80
+ agent: this.id,
81
+ ...this.languageModelRequirements[0]
82
+ });
83
+ if (!lm) {
84
+ throw new Error('No language model available for the Commit Message agent.');
85
+ }
86
+
87
+ const parameters = { changes };
88
+ const systemMessage = await this.promptService.getResolvedPromptFragmentWithoutFunctions(COMMIT_MESSAGE_SYSTEM_PROMPT_ID, parameters).then(p => p?.text);
89
+ const userMessage = await this.promptService.getResolvedPromptFragmentWithoutFunctions(COMMIT_MESSAGE_USER_PROMPT_ID, parameters).then(p => p?.text);
90
+ if (!systemMessage || !userMessage) {
91
+ throw new Error('The prompt service did not return prompts for the Commit Message agent.');
92
+ }
93
+
94
+ const variantInfo = this.promptService.getPromptVariantInfo(COMMIT_MESSAGE_SYSTEM_PROMPT_ID);
95
+
96
+ const request: UserRequest = {
97
+ messages: [
98
+ { actor: 'system', type: 'text', text: systemMessage },
99
+ { actor: 'user', type: 'text', text: userMessage }
100
+ ],
101
+ agentId: this.id,
102
+ sessionId: generateUuid(),
103
+ requestId: generateUuid(),
104
+ cancellationToken,
105
+ promptVariantId: variantInfo?.variantId,
106
+ isPromptVariantCustomized: variantInfo?.isCustomized
107
+ };
108
+
109
+ const result = await this.languageModelService.sendRequest(lm, request);
110
+ const text = await getTextOfResponse(result);
111
+ return text.trim();
112
+ }
113
+ }
@@ -0,0 +1,67 @@
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 { CommandContribution, CommandRegistry } from '@theia/core';
18
+ import { inject, injectable } from '@theia/core/shared/inversify';
19
+ import { AgentService } from '@theia/ai-core/lib/common';
20
+ import { AICommandHandlerFactory } from '@theia/ai-core/lib/browser';
21
+ import { ScmService } from '@theia/scm/lib/browser/scm-service';
22
+ import { COMMIT_MESSAGE_AGENT_ID } from './commit-message-agent';
23
+ import { CommitMessageCommands } from './commit-message-commands';
24
+ import { CommitMessageRunner } from './commit-message-runner';
25
+
26
+ @injectable()
27
+ export class CommitMessageCommandContribution implements CommandContribution {
28
+
29
+ @inject(CommitMessageRunner)
30
+ protected readonly runner: CommitMessageRunner;
31
+
32
+ @inject(ScmService)
33
+ protected readonly scmService: ScmService;
34
+
35
+ @inject(AgentService)
36
+ protected readonly agentService: AgentService;
37
+
38
+ @inject(AICommandHandlerFactory)
39
+ protected readonly commandHandlerFactory: AICommandHandlerFactory;
40
+
41
+ registerCommands(registry: CommandRegistry): void {
42
+ registry.registerCommand(CommitMessageCommands.GENERATE_FROM_STAGED, this.commandHandlerFactory({
43
+ execute: () => this.execute(),
44
+ isEnabled: () => this.isEnabled()
45
+ }));
46
+ }
47
+
48
+ protected execute(): void | Promise<void> {
49
+ if (this.runner.isRunning()) {
50
+ this.runner.cancel();
51
+ return;
52
+ }
53
+ return this.runner.run();
54
+ }
55
+
56
+ protected isEnabled(): boolean {
57
+ // While a generation is in flight the command cancels it, which must stay possible even
58
+ // once the git extension has disabled the commit input for an ongoing commit.
59
+ if (this.runner.isRunning()) {
60
+ return true;
61
+ }
62
+ const repository = this.scmService.selectedRepository;
63
+ return !!repository
64
+ && repository.input.enabled
65
+ && this.agentService.isEnabled(COMMIT_MESSAGE_AGENT_ID);
66
+ }
67
+ }
@@ -0,0 +1,25 @@
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 { Command } from '@theia/core';
18
+
19
+ export namespace CommitMessageCommands {
20
+ export const GENERATE_FROM_STAGED: Command = Command.toLocalizedCommand({
21
+ id: 'ai-ide.commit-message.generate-from-staged',
22
+ category: 'AI',
23
+ label: 'Generate Commit Message from Staged Changes'
24
+ }, 'theia/ai-ide/commit-message/generate-from-staged');
25
+ }
@@ -0,0 +1,65 @@
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 { BasePromptFragment } from '@theia/ai-core/lib/common';
18
+
19
+ export const COMMIT_MESSAGE_SYSTEM_PROMPT_ID = 'commit-message-system';
20
+ export const COMMIT_MESSAGE_USER_PROMPT_ID = 'commit-message-user';
21
+
22
+ export const commitMessageSystemPrompt: BasePromptFragment = {
23
+ id: COMMIT_MESSAGE_SYSTEM_PROMPT_ID,
24
+ template: `# Role
25
+
26
+ You are an assistant that produces **git commit messages** from a unified diff of changes in a
27
+ workspace's git repository. The diff is provided to you directly in the user message.
28
+
29
+ # Output Format
30
+
31
+ Return ONLY the commit message text. Nothing else.
32
+
33
+ - **No** markdown headings, no quotes, no code fences, no preamble like "Here is …".
34
+ - First line is the **subject**:
35
+ - imperative mood ("Add", "Fix", "Refactor", not "Added"/"Adds")
36
+ - prefer the **Conventional Commits** style where appropriate, e.g. \`feat:\`, \`fix:\`, \`refactor:\`, \`docs:\`, \`test:\`, \`chore:\`, \`build:\`, \`ci:\`, \`perf:\`
37
+ - at most 72 characters
38
+ - no trailing period
39
+ - If a body is needed to explain the *why* or non-obvious *what*, separate it from the subject with a single blank line.
40
+ Wrap body lines at ~72 characters. Use bullet lists (\`- …\`) for multiple changes.
41
+ - Keep the message concise. Do not invent context that is not visible in the diff.
42
+
43
+ # Constraints
44
+
45
+ - Base the message only on the provided diff. Never include the diff itself in the response.
46
+ - Never include sign-off lines, issue numbers, or co-authored-by trailers unless they are
47
+ clearly present in the provided changes.
48
+ - If the provided diff is empty or you cannot determine a meaningful message, respond with exactly: \`No changes to commit.\`
49
+ `
50
+ };
51
+
52
+ export const commitMessageUserPrompt: BasePromptFragment = {
53
+ id: COMMIT_MESSAGE_USER_PROMPT_ID,
54
+ template: `Generate a commit message for the following staged changes.
55
+
56
+ \`\`\`diff
57
+ {{changes}}
58
+ \`\`\`
59
+ `
60
+ };
61
+
62
+ export const commitMessagePrompts = [
63
+ { id: COMMIT_MESSAGE_SYSTEM_PROMPT_ID, defaultVariant: commitMessageSystemPrompt, variants: [] },
64
+ { id: COMMIT_MESSAGE_USER_PROMPT_ID, defaultVariant: commitMessageUserPrompt, variants: [] }
65
+ ];
@@ -0,0 +1,266 @@
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 { CancellationToken, ILogger, MessageService } from '@theia/core';
26
+ import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
27
+ import { ScmService } from '@theia/scm/lib/browser/scm-service';
28
+ import { ScmInput } from '@theia/scm/lib/browser/scm-input';
29
+ import { CommitMessageAgent } from './commit-message-agent';
30
+ import { CommitMessageRunner } from './commit-message-runner';
31
+ import { GetGitChangesTool } from './git-changes-tool';
32
+
33
+ disableJSDOM();
34
+
35
+ interface MockRepository {
36
+ input: ScmInput;
37
+ provider: { rootUri: string };
38
+ }
39
+
40
+ describe('CommitMessageRunner', () => {
41
+
42
+ let container: Container;
43
+ let runner: CommitMessageRunner;
44
+ let agent: { generateCommitMessage: sinon.SinonStub };
45
+ let gitChangesTool: { getStagedChanges: sinon.SinonStub };
46
+ let scmService: { selectedRepository: MockRepository | undefined };
47
+ let messageService: { warn: sinon.SinonStub; error: sinon.SinonStub; info: sinon.SinonStub };
48
+ let logger: { error: sinon.SinonStub; warn: sinon.SinonStub; info: sinon.SinonStub; debug: sinon.SinonStub; trace: sinon.SinonStub };
49
+ let repository: MockRepository;
50
+ let focusSpy: sinon.SinonSpy;
51
+ let confirmDialogOpen: sinon.SinonStub;
52
+
53
+ before(() => {
54
+ disableJSDOM = enableJSDOM();
55
+ });
56
+
57
+ after(() => {
58
+ disableJSDOM();
59
+ });
60
+
61
+ beforeEach(() => {
62
+ container = new Container();
63
+
64
+ const input = new ScmInput();
65
+ focusSpy = sinon.spy(input, 'focus');
66
+ repository = { input, provider: { rootUri: 'file:///workspace-root' } };
67
+
68
+ scmService = { selectedRepository: repository };
69
+ agent = { generateCommitMessage: sinon.stub().resolves('feat: add new thing') };
70
+ gitChangesTool = { getStagedChanges: sinon.stub().resolves('diff --git a b') };
71
+ messageService = {
72
+ warn: sinon.stub().resolves(undefined),
73
+ error: sinon.stub().resolves(undefined),
74
+ info: sinon.stub().resolves(undefined)
75
+ };
76
+ logger = {
77
+ error: sinon.stub(),
78
+ warn: sinon.stub(),
79
+ info: sinon.stub(),
80
+ debug: sinon.stub(),
81
+ trace: sinon.stub()
82
+ };
83
+ // The overwrite prompt is a modal `ConfirmDialog`, not a toast, so it is stubbed at the
84
+ // prototype instead of via `MessageService`.
85
+ confirmDialogOpen = sinon.stub(ConfirmDialog.prototype, 'open').resolves(true);
86
+
87
+ container.bind(CommitMessageAgent).toConstantValue(agent as unknown as CommitMessageAgent);
88
+ container.bind(GetGitChangesTool).toConstantValue(gitChangesTool as unknown as GetGitChangesTool);
89
+ container.bind(ScmService).toConstantValue(scmService as unknown as ScmService);
90
+ container.bind(MessageService).toConstantValue(messageService as unknown as MessageService);
91
+ container.bind(ILogger).toConstantValue(logger as unknown as ILogger);
92
+ container.bind(CommitMessageRunner).toSelf();
93
+
94
+ runner = container.get(CommitMessageRunner);
95
+ });
96
+
97
+ afterEach(() => sinon.restore());
98
+
99
+ it('writes the generated message into the SCM input and focuses it on success', async () => {
100
+ await runner.run();
101
+
102
+ expect(gitChangesTool.getStagedChanges.calledOnce).to.be.true;
103
+ // The diff is read for the selected repository, bypassing the model-facing tool call.
104
+ expect(gitChangesTool.getStagedChanges.firstCall.args[0]).to.equal(repository);
105
+ expect(agent.generateCommitMessage.calledOnce).to.be.true;
106
+ expect(repository.input.value).to.equal('feat: add new thing');
107
+ expect(focusSpy.calledOnce).to.be.true;
108
+ expect(messageService.error.called).to.be.false;
109
+ });
110
+
111
+ it('warns and does not invoke the agent when no repository is selected', async () => {
112
+ scmService.selectedRepository = undefined;
113
+
114
+ await runner.run();
115
+
116
+ expect(messageService.warn.calledOnce).to.be.true;
117
+ expect(gitChangesTool.getStagedChanges.called).to.be.false;
118
+ expect(agent.generateCommitMessage.called).to.be.false;
119
+ });
120
+
121
+ it('does not await notifications, so a dismissed toast cannot leave the run hanging', async () => {
122
+ scmService.selectedRepository = undefined;
123
+ // A toast the user never interacts with never settles; awaiting it would hang `run`.
124
+ messageService.warn.returns(new Promise<undefined>(() => { /* never settles */ }));
125
+
126
+ await runner.run();
127
+
128
+ expect(runner.isRunning()).to.be.false;
129
+ });
130
+
131
+ it('warns and does not invoke the agent when there are no staged changes', async () => {
132
+ gitChangesTool.getStagedChanges.resolves(' \n ');
133
+
134
+ await runner.run();
135
+
136
+ expect(messageService.warn.calledOnce).to.be.true;
137
+ expect(agent.generateCommitMessage.called).to.be.false;
138
+ expect(repository.input.value).to.equal('');
139
+ });
140
+
141
+ it('skips the overwrite prompt when the commit field is empty', async () => {
142
+ repository.input.value = '';
143
+
144
+ await runner.run();
145
+
146
+ expect(confirmDialogOpen.called).to.be.false;
147
+ expect(repository.input.value).to.equal('feat: add new thing');
148
+ });
149
+
150
+ it('asks for confirmation in a modal dialog and overwrites when the user accepts', async () => {
151
+ repository.input.value = 'existing text';
152
+
153
+ await runner.run();
154
+
155
+ expect(confirmDialogOpen.calledOnce).to.be.true;
156
+ expect(repository.input.value).to.equal('feat: add new thing');
157
+ });
158
+
159
+ it('aborts without invoking the agent when the user cancels the overwrite prompt', async () => {
160
+ repository.input.value = 'existing text';
161
+ confirmDialogOpen.resolves(undefined);
162
+
163
+ await runner.run();
164
+
165
+ expect(agent.generateCommitMessage.called).to.be.false;
166
+ expect(repository.input.value).to.equal('existing text');
167
+ });
168
+
169
+ it('does not invoke the agent when the run is canceled while the overwrite prompt is open', async () => {
170
+ repository.input.value = 'existing text';
171
+ let confirm: (replace: boolean) => void = () => { /* set below */ };
172
+ confirmDialogOpen.callsFake(() => new Promise<boolean>(resolve => { confirm = resolve; }));
173
+
174
+ const runPromise = runner.run();
175
+ await new Promise(resolve => setTimeout(resolve, 0));
176
+
177
+ runner.cancel();
178
+ confirm(true);
179
+ await runPromise;
180
+
181
+ expect(agent.generateCommitMessage.called).to.be.false;
182
+ expect(repository.input.value).to.equal('existing text');
183
+ });
184
+
185
+ it('shows an error notification when the agent fails', async () => {
186
+ agent.generateCommitMessage.rejects(new Error('boom'));
187
+
188
+ await runner.run();
189
+
190
+ expect(messageService.error.calledOnce).to.be.true;
191
+ expect(messageService.error.firstCall.args[0]).to.contain('boom');
192
+ expect(repository.input.value).to.equal('');
193
+ });
194
+
195
+ it('shows an error notification when reading the staged changes fails', async () => {
196
+ gitChangesTool.getStagedChanges.rejects(new Error('fatal: not a git repository'));
197
+
198
+ await runner.run();
199
+
200
+ expect(messageService.error.calledOnce).to.be.true;
201
+ expect(messageService.error.firstCall.args[0]).to.contain('fatal: not a git repository');
202
+ expect(agent.generateCommitMessage.called).to.be.false;
203
+ });
204
+
205
+ it('warns when the model returns an empty message', async () => {
206
+ agent.generateCommitMessage.resolves('');
207
+
208
+ await runner.run();
209
+
210
+ expect(messageService.warn.calledOnce).to.be.true;
211
+ expect(repository.input.value).to.equal('');
212
+ });
213
+
214
+ it('does not change the input or notify when the run is canceled mid-generation', async () => {
215
+ let resolveGeneration: (message: string) => void = () => { /* set below */ };
216
+ agent.generateCommitMessage.callsFake(() => new Promise<string>(resolve => { resolveGeneration = resolve; }));
217
+
218
+ const runPromise = runner.run();
219
+ await new Promise(resolve => setTimeout(resolve, 0));
220
+ expect(runner.isRunning()).to.be.true;
221
+
222
+ runner.cancel();
223
+ resolveGeneration('feat: ignored');
224
+ await runPromise;
225
+
226
+ expect(repository.input.value).to.equal('');
227
+ expect(messageService.error.called).to.be.false;
228
+ expect(runner.isRunning()).to.be.false;
229
+ });
230
+
231
+ it('ignores a second run while one is still in flight', async () => {
232
+ let resolveChanges: (diff: string) => void = () => { /* set below */ };
233
+ gitChangesTool.getStagedChanges.callsFake(() => new Promise<string>(resolve => { resolveChanges = resolve; }));
234
+
235
+ const firstRun = runner.run();
236
+ await new Promise(resolve => setTimeout(resolve, 0));
237
+ expect(runner.isRunning()).to.be.true;
238
+
239
+ await runner.run();
240
+ expect(gitChangesTool.getStagedChanges.calledOnce).to.be.true;
241
+
242
+ resolveChanges('diff --git a b');
243
+ await firstRun;
244
+ expect(runner.isRunning()).to.be.false;
245
+ });
246
+
247
+ it('passes its cancellation token down so the git command can be aborted', async () => {
248
+ let capturedToken: CancellationToken | undefined;
249
+ // Mirror the tool: a canceled execution resolves with an empty diff.
250
+ gitChangesTool.getStagedChanges.callsFake((_repository: unknown, token: CancellationToken) => {
251
+ capturedToken = token;
252
+ return new Promise<string>(resolve => token.onCancellationRequested(() => resolve('')));
253
+ });
254
+
255
+ const runPromise = runner.run();
256
+ await new Promise(resolve => setTimeout(resolve, 0));
257
+ expect(capturedToken?.isCancellationRequested).to.be.false;
258
+
259
+ runner.cancel();
260
+ await runPromise;
261
+
262
+ expect(capturedToken?.isCancellationRequested).to.be.true;
263
+ expect(agent.generateCommitMessage.called).to.be.false;
264
+ expect(runner.isRunning()).to.be.false;
265
+ });
266
+ });