@theia/ai-core 1.77.0-next.49 → 1.77.0-next.57

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 (102) hide show
  1. package/lib/browser/agents-md-frontend-application-contribution.d.ts +65 -0
  2. package/lib/browser/agents-md-frontend-application-contribution.d.ts.map +1 -0
  3. package/lib/browser/agents-md-frontend-application-contribution.js +269 -0
  4. package/lib/browser/agents-md-frontend-application-contribution.js.map +1 -0
  5. package/lib/browser/agents-md-frontend-application-contribution.spec.d.ts +2 -0
  6. package/lib/browser/agents-md-frontend-application-contribution.spec.d.ts.map +1 -0
  7. package/lib/browser/agents-md-frontend-application-contribution.spec.js +225 -0
  8. package/lib/browser/agents-md-frontend-application-contribution.spec.js.map +1 -0
  9. package/lib/browser/agents-md-migration-service.d.ts +78 -0
  10. package/lib/browser/agents-md-migration-service.d.ts.map +1 -0
  11. package/lib/browser/agents-md-migration-service.js +195 -0
  12. package/lib/browser/agents-md-migration-service.js.map +1 -0
  13. package/lib/browser/agents-md-migration-service.spec.d.ts +2 -0
  14. package/lib/browser/agents-md-migration-service.spec.d.ts.map +1 -0
  15. package/lib/browser/agents-md-migration-service.spec.js +278 -0
  16. package/lib/browser/agents-md-migration-service.spec.js.map +1 -0
  17. package/lib/browser/agents-md-prompt-fragment-contribution.d.ts +13 -0
  18. package/lib/browser/agents-md-prompt-fragment-contribution.d.ts.map +1 -0
  19. package/lib/browser/agents-md-prompt-fragment-contribution.js +76 -0
  20. package/lib/browser/agents-md-prompt-fragment-contribution.js.map +1 -0
  21. package/lib/browser/agents-md-service.d.ts +58 -0
  22. package/lib/browser/agents-md-service.d.ts.map +1 -0
  23. package/lib/browser/agents-md-service.js +191 -0
  24. package/lib/browser/agents-md-service.js.map +1 -0
  25. package/lib/browser/agents-md-service.spec.d.ts +2 -0
  26. package/lib/browser/agents-md-service.spec.d.ts.map +1 -0
  27. package/lib/browser/agents-md-service.spec.js +220 -0
  28. package/lib/browser/agents-md-service.spec.js.map +1 -0
  29. package/lib/browser/agents-md-variable-contribution.d.ts +66 -0
  30. package/lib/browser/agents-md-variable-contribution.d.ts.map +1 -0
  31. package/lib/browser/agents-md-variable-contribution.js +169 -0
  32. package/lib/browser/agents-md-variable-contribution.js.map +1 -0
  33. package/lib/browser/agents-md-variable-contribution.spec.d.ts +2 -0
  34. package/lib/browser/agents-md-variable-contribution.spec.d.ts.map +1 -0
  35. package/lib/browser/agents-md-variable-contribution.spec.js +148 -0
  36. package/lib/browser/agents-md-variable-contribution.spec.js.map +1 -0
  37. package/lib/browser/ai-core-frontend-module.d.ts.map +1 -1
  38. package/lib/browser/ai-core-frontend-module.js +15 -0
  39. package/lib/browser/ai-core-frontend-module.js.map +1 -1
  40. package/lib/browser/frontend-prompt-customization-service.d.ts +2 -1
  41. package/lib/browser/frontend-prompt-customization-service.d.ts.map +1 -1
  42. package/lib/browser/frontend-prompt-customization-service.js +10 -0
  43. package/lib/browser/frontend-prompt-customization-service.js.map +1 -1
  44. package/lib/browser/frontend-prompt-customization-service.spec.js +19 -1
  45. package/lib/browser/frontend-prompt-customization-service.spec.js.map +1 -1
  46. package/lib/browser/frontend-variable-service.js +1 -1
  47. package/lib/browser/frontend-variable-service.js.map +1 -1
  48. package/lib/browser/index.d.ts +5 -0
  49. package/lib/browser/index.d.ts.map +1 -1
  50. package/lib/browser/index.js +5 -0
  51. package/lib/browser/index.js.map +1 -1
  52. package/lib/browser/prompt-variable-contribution.js +1 -1
  53. package/lib/browser/prompt-variable-contribution.js.map +1 -1
  54. package/lib/browser/prompt-variable-contribution.spec.js +2 -1
  55. package/lib/browser/prompt-variable-contribution.spec.js.map +1 -1
  56. package/lib/browser/prompttemplate-contribution.js +1 -1
  57. package/lib/browser/prompttemplate-contribution.js.map +1 -1
  58. package/lib/browser/skill-service.spec.js +2 -1
  59. package/lib/browser/skill-service.spec.js.map +1 -1
  60. package/lib/browser/skills-variable-contribution.spec.js +2 -1
  61. package/lib/browser/skills-variable-contribution.spec.js.map +1 -1
  62. package/lib/common/agents-md.d.ts +20 -0
  63. package/lib/common/agents-md.d.ts.map +1 -0
  64. package/lib/common/agents-md.js +38 -0
  65. package/lib/common/agents-md.js.map +1 -0
  66. package/lib/common/ai-variable-resource.js +3 -3
  67. package/lib/common/ai-variable-resource.js.map +1 -1
  68. package/lib/common/capability-variable-contribution.js +1 -1
  69. package/lib/common/capability-variable-contribution.js.map +1 -1
  70. package/lib/common/index.d.ts +1 -0
  71. package/lib/common/index.d.ts.map +1 -1
  72. package/lib/common/index.js +1 -0
  73. package/lib/common/index.js.map +1 -1
  74. package/lib/common/prompt-service.d.ts +12 -0
  75. package/lib/common/prompt-service.d.ts.map +1 -1
  76. package/lib/common/prompt-service.js.map +1 -1
  77. package/lib/common/prompt-service.spec.js +2 -1
  78. package/lib/common/prompt-service.spec.js.map +1 -1
  79. package/lib/common/variable-service.spec.js +2 -1
  80. package/lib/common/variable-service.spec.js.map +1 -1
  81. package/lib/node/model-snapshot-store.js +2 -2
  82. package/lib/node/model-snapshot-store.js.map +1 -1
  83. package/lib/node/proxy-util.js +2 -1
  84. package/lib/node/proxy-util.js.map +1 -1
  85. package/package.json +9 -9
  86. package/src/browser/agents-md-frontend-application-contribution.spec.ts +244 -0
  87. package/src/browser/agents-md-frontend-application-contribution.ts +291 -0
  88. package/src/browser/agents-md-migration-service.spec.ts +338 -0
  89. package/src/browser/agents-md-migration-service.ts +226 -0
  90. package/src/browser/agents-md-prompt-fragment-contribution.ts +73 -0
  91. package/src/browser/agents-md-service.spec.ts +258 -0
  92. package/src/browser/agents-md-service.ts +212 -0
  93. package/src/browser/agents-md-variable-contribution.spec.ts +180 -0
  94. package/src/browser/agents-md-variable-contribution.ts +182 -0
  95. package/src/browser/ai-core-frontend-module.ts +17 -0
  96. package/src/browser/frontend-prompt-customization-service.spec.ts +24 -0
  97. package/src/browser/frontend-prompt-customization-service.ts +13 -1
  98. package/src/browser/index.ts +5 -0
  99. package/src/common/agents-md.ts +37 -0
  100. package/src/common/ai-variable-resource.ts +1 -1
  101. package/src/common/index.ts +1 -0
  102. package/src/common/prompt-service.ts +14 -0
@@ -0,0 +1,244 @@
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
+ const disableJSDOM = enableJSDOM();
19
+ import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
20
+ try {
21
+ FrontendApplicationConfigProvider.get();
22
+ } catch {
23
+ FrontendApplicationConfigProvider.set({});
24
+ }
25
+
26
+ import { expect } from 'chai';
27
+ import * as sinon from 'sinon';
28
+ import { Emitter, URI } from '@theia/core';
29
+ import { AgentsMdFrontendApplicationContribution } from './agents-md-frontend-application-contribution';
30
+ import { LEGACY_PROJECT_INFO_PATH } from './agents-md-migration-service';
31
+ import { PromptFragmentCustomizationSource } from '../common/prompt-service';
32
+
33
+ disableJSDOM();
34
+
35
+ class TestContribution extends AgentsMdFrontendApplicationContribution {
36
+ async checkOverrides(): Promise<void> {
37
+ this.scheduleOverrideWarning();
38
+ await this.settleWarnings();
39
+ }
40
+
41
+ async settleWarnings(): Promise<void> {
42
+ await this.startupMigration;
43
+ // Let the debounce timer, which has no delay in these tests, fire.
44
+ await new Promise(resolve => setTimeout(resolve, 0));
45
+ await this.overrideWarningUpdates;
46
+ // Allow the notification action's asynchronous handler to finish.
47
+ await Promise.resolve();
48
+ await Promise.resolve();
49
+ }
50
+ }
51
+
52
+ describe('AgentsMdFrontendApplicationContribution - legacy overrides', () => {
53
+ let sources: PromptFragmentCustomizationSource[];
54
+ let trusted: boolean;
55
+ let roots: { resource: URI }[];
56
+ let stored: number | undefined;
57
+ let choice: string | undefined;
58
+ let contribution: TestContribution;
59
+ let warning: sinon.SinonStub;
60
+ let info: sinon.SinonStub;
61
+ let opener: sinon.SinonStub;
62
+ let writeStorage: sinon.SinonStub;
63
+ let migration: sinon.SinonStub;
64
+ let scanCompleted: Emitter<void>;
65
+ let fragmentsChanged: Emitter<string[]>;
66
+ let trustChanged: Emitter<boolean>;
67
+
68
+ function source(path: string, template = 'Instructions', active = true): PromptFragmentCustomizationSource {
69
+ return { uri: URI.fromFilePath(path), template, active };
70
+ }
71
+
72
+ function createContribution(): TestContribution {
73
+ const result = new TestContribution();
74
+ Object.assign(result, {
75
+ workspaceTrustService: { getWorkspaceTrust: async () => trusted, onDidChangeWorkspaceTrust: trustChanged.event },
76
+ workspaceService: { ready: Promise.resolve(), tryGetRoots: () => roots },
77
+ customizationService: {
78
+ getPromptFragmentCustomizationSources: () => sources,
79
+ onDidChangeCustomAgents: scanCompleted.event,
80
+ onDidChangePromptFragmentCustomization: fragmentsChanged.event
81
+ },
82
+ storageService: { getData: async () => stored, setData: writeStorage },
83
+ messageService: { warn: warning, info },
84
+ openerService: { getOpener: async () => ({ open: opener }) },
85
+ migrationService: { migrate: migration },
86
+ appStateService: { reachedState: async () => { } },
87
+ logger: { warn: sinon.stub() },
88
+ overrideWarningDelay: 0,
89
+ overrideWarningEnabled: true
90
+ });
91
+ return result;
92
+ }
93
+
94
+ beforeEach(() => {
95
+ sources = [];
96
+ trusted = true;
97
+ roots = [{ resource: URI.fromFilePath('/ws') }];
98
+ stored = undefined;
99
+ choice = undefined;
100
+ warning = sinon.stub().callsFake(async () => choice);
101
+ info = sinon.stub().resolves();
102
+ opener = sinon.stub().resolves();
103
+ writeStorage = sinon.stub().callsFake(async (_key: string, value: number) => { stored = value; });
104
+ migration = sinon.stub().resolves([]);
105
+ scanCompleted = new Emitter<void>();
106
+ fragmentsChanged = new Emitter<string[]>();
107
+ trustChanged = new Emitter<boolean>();
108
+ contribution = createContribution();
109
+ });
110
+
111
+ afterEach(() => {
112
+ contribution.onStop();
113
+ scanCompleted.dispose();
114
+ fragmentsChanged.dispose();
115
+ trustChanged.dispose();
116
+ });
117
+
118
+ it('leaves canonical local files to the automatic migration in every root', async () => {
119
+ roots.push({ resource: URI.fromFilePath('/other') });
120
+ sources = [source(`/ws/${LEGACY_PROJECT_INFO_PATH}`), source(`/other/${LEGACY_PROJECT_INFO_PATH}`)];
121
+ await contribution.checkOverrides();
122
+ expect(warning.called).to.be.false;
123
+ });
124
+
125
+ it('lists configured, shared and shadowed sources with their active status', async () => {
126
+ sources = [
127
+ source('/config/project-info.prompttemplate', 'Shared', false),
128
+ source('/ws/.my-prompts/project-info.prompttemplate'),
129
+ source(`/ws/${LEGACY_PROJECT_INFO_PATH}`)
130
+ ];
131
+ await contribution.checkOverrides();
132
+ const message = warning.firstCall.args[0];
133
+ expect(message).to.contain('`/config/project-info.prompttemplate` (Inactive), `/ws/.my-prompts/project-info.prompttemplate` (Active)');
134
+ expect(message).not.to.contain('\n');
135
+ expect(message).not.to.contain(`/ws/${LEGACY_PROJECT_INFO_PATH}`);
136
+ expect(message).to.contain('still apply to prompts that reference project-info');
137
+ });
138
+
139
+ it('opens every reported source without treating opening as persistent dismissal', async () => {
140
+ sources = [source('/config/project-info.prompttemplate'), source('/ws/custom/project-info.prompttemplate')];
141
+ choice = 'Open Source Files';
142
+ await contribution.checkOverrides();
143
+ expect(opener.args.map(args => args[0].path.toString())).to.deep.equal(sources.map(entry => entry.uri.path.toString()));
144
+ expect(writeStorage.called).to.be.false;
145
+ });
146
+
147
+ it('remembers dismissal across restarts and ignores source ordering', async () => {
148
+ sources = [source('/config/project-info.prompttemplate'), source('/ws/custom/project-info.prompttemplate')];
149
+ await contribution.checkOverrides();
150
+ expect(writeStorage.calledOnce).to.be.true;
151
+ sources.reverse();
152
+ contribution = createContribution();
153
+ await contribution.checkOverrides();
154
+ expect(warning.calledOnce).to.be.true;
155
+ });
156
+
157
+ it('warns again when content or source paths change, but not for the active status alone', async () => {
158
+ sources = [source('/config/project-info.prompttemplate')];
159
+ await contribution.checkOverrides();
160
+ sources[0].template = 'Changed instructions';
161
+ await contribution.checkOverrides();
162
+ sources[0].uri = URI.fromFilePath('/config/other/project-info.prompttemplate');
163
+ await contribution.checkOverrides();
164
+ sources[0].active = false;
165
+ await contribution.checkOverrides();
166
+ expect(warning.callCount).to.equal(3);
167
+ });
168
+
169
+ it('reports once for a burst of rescans', async () => {
170
+ sources = [source('/config/project-info.prompttemplate')];
171
+ contribution.onStart();
172
+ scanCompleted.fire();
173
+ sources[0].active = false;
174
+ fragmentsChanged.fire(['project-info']);
175
+ scanCompleted.fire();
176
+ await contribution.settleWarnings();
177
+ expect(warning.calledOnce).to.be.true;
178
+ expect(warning.firstCall.args[0]).to.contain('(Inactive)');
179
+ });
180
+
181
+ it('does not report anything before the application is ready', async () => {
182
+ sources = [source('/config/project-info.prompttemplate')];
183
+ contribution = createContribution();
184
+ Object.assign(contribution, { overrideWarningEnabled: false, appStateService: { reachedState: () => new Promise(() => { }) } });
185
+ contribution.onStart();
186
+ scanCompleted.fire();
187
+ await contribution.settleWarnings();
188
+ expect(warning.called).to.be.false;
189
+ });
190
+
191
+ it('reports leftover prompt syntax in a warning of its own', async () => {
192
+ migration.resolves([{ root: URI.fromFilePath('/ws'), migrated: true, alreadyPresent: false, backedUp: true, containsPromptSyntax: true }]);
193
+ await (contribution as unknown as { runMigration(reportEmptyResult: boolean): Promise<void> }).runMigration(false);
194
+ expect(info.firstCall.args[0]).not.to.contain('prompt template syntax');
195
+ expect(warning.firstCall.args[0]).to.contain('prompt template syntax');
196
+ });
197
+
198
+ it('tells the user that an untrusted workspace was not migrated', async () => {
199
+ trusted = false;
200
+ await (contribution as unknown as { runMigration(reportEmptyResult: boolean): Promise<void> }).runMigration(true);
201
+ expect(info.firstCall.args[0]).to.contain('not trusted');
202
+ });
203
+
204
+ it('does not show duplicates while a notification is pending', async () => {
205
+ sources = [source('/config/project-info.prompttemplate')];
206
+ warning.returns(new Promise<string | undefined>(() => {}));
207
+ await contribution.checkOverrides();
208
+ await contribution.checkOverrides();
209
+ expect(warning.calledOnce).to.be.true;
210
+ });
211
+
212
+ it('does not reuse dismissal in a different workspace', async () => {
213
+ sources = [source('/config/project-info.prompttemplate')];
214
+ await contribution.checkOverrides();
215
+ roots = [{ resource: URI.fromFilePath('/other') }];
216
+ await contribution.checkOverrides();
217
+ expect(warning.calledTwice).to.be.true;
218
+ });
219
+
220
+ it('does not warn for an untrusted or empty workspace', async () => {
221
+ sources = [source('/config/project-info.prompttemplate')];
222
+ trusted = false;
223
+ await contribution.checkOverrides();
224
+ trusted = true;
225
+ roots = [];
226
+ await contribution.checkOverrides();
227
+ expect(warning.called).to.be.false;
228
+ });
229
+
230
+ it('detects overrides when the initial template scan finishes after migration', async () => {
231
+ contribution.onStart();
232
+ await contribution.settleWarnings();
233
+ expect(migration.calledOnce).to.be.true;
234
+ expect(warning.called).to.be.false;
235
+ sources = [source('/config/project-info.prompttemplate')];
236
+ scanCompleted.fire();
237
+ await contribution.settleWarnings();
238
+ expect(warning.calledOnce).to.be.true;
239
+ sources[0].template = 'Updated';
240
+ fragmentsChanged.fire(['project-info']);
241
+ await contribution.settleWarnings();
242
+ expect(warning.calledTwice).to.be.true;
243
+ });
244
+ });
@@ -0,0 +1,291 @@
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 { inject, injectable, named, optional } from '@theia/core/shared/inversify';
18
+ import { Command, CommandContribution, CommandRegistry, DisposableCollection, ILogger, MessageService, nls } from '@theia/core';
19
+ import { FrontendApplicationContribution, OpenerService, open } from '@theia/core/lib/browser';
20
+ import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
21
+ import { WorkspaceTrustService } from '@theia/workspace/lib/browser/workspace-trust-service';
22
+ import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
23
+ import { WorkspaceStorageService } from '@theia/workspace/lib/browser/workspace-storage-service';
24
+ import { hash } from '@theia/core/lib/common/hash';
25
+ import { PromptFragmentCustomizationService } from '../common/prompt-service';
26
+ import { AGENTS_MD_FILE_NAME, PROJECT_INFO_PROMPT_FRAGMENT_ID } from '../common/agents-md';
27
+ import { AgentsMdMigrationReport, AgentsMdMigrationService, LEGACY_PROJECT_INFO_BACKUP_PATH, LEGACY_PROJECT_INFO_PATH } from './agents-md-migration-service';
28
+
29
+ export const RERUN_AGENTS_MD_MIGRATION_COMMAND: Command = Command.toLocalizedCommand(
30
+ {
31
+ id: 'ai-core.agentsMd.rerunMigration',
32
+ label: 'Re-run AGENTS.md migration',
33
+ category: 'AI'
34
+ },
35
+ 'theia/ai/core/agentsMd/rerunMigration',
36
+ 'theia/ai/core/category'
37
+ );
38
+
39
+ /**
40
+ * Runs the `AGENTS.md` migration on startup and exposes it as a command.
41
+ *
42
+ * The migration writes into the workspace root, a file the user will see in source control, so
43
+ * unlike the silent `customAgents.yml` migration it always reports what it did. Migration is
44
+ * re-attempted when the workspace becomes trusted; roots added later are covered by the command.
45
+ */
46
+ @injectable()
47
+ export class AgentsMdFrontendApplicationContribution implements FrontendApplicationContribution, CommandContribution {
48
+
49
+ @inject(AgentsMdMigrationService)
50
+ protected readonly migrationService: AgentsMdMigrationService;
51
+
52
+ @inject(WorkspaceTrustService)
53
+ protected readonly workspaceTrustService: WorkspaceTrustService;
54
+
55
+ @inject(WorkspaceService)
56
+ protected readonly workspaceService: WorkspaceService;
57
+
58
+ @inject(WorkspaceStorageService)
59
+ protected readonly storageService: WorkspaceStorageService;
60
+
61
+ @inject(PromptFragmentCustomizationService)
62
+ protected readonly customizationService: PromptFragmentCustomizationService;
63
+
64
+ @inject(FrontendApplicationStateService)
65
+ protected readonly appStateService: FrontendApplicationStateService;
66
+
67
+ @inject(OpenerService)
68
+ protected readonly openerService: OpenerService;
69
+
70
+ @inject(MessageService) @optional()
71
+ protected readonly messageService?: MessageService;
72
+
73
+ @inject(ILogger) @named('ai-core:AgentsMdFrontendApplicationContribution')
74
+ protected readonly logger: ILogger;
75
+
76
+ /** In-flight run started by {@link onStart} or by the initial trust resolution. */
77
+ protected startupMigration: Promise<void> | undefined;
78
+
79
+ /**
80
+ * How long the prompt customizations have to be stable before overrides are reported. The
81
+ * workspace template locations are configured only after the initial scan of the global ones, and
82
+ * each rescan fires several events; reporting on the first of them would show a notification for a
83
+ * state that is about to change.
84
+ */
85
+ protected overrideWarningDelay = 1000;
86
+ protected overrideWarningTimer: ReturnType<typeof setTimeout> | undefined;
87
+ /** Set once the application and the workspace trust are ready, the earliest a report makes sense. */
88
+ protected overrideWarningEnabled = false;
89
+ protected overrideWarningUpdates: Promise<void> = Promise.resolve();
90
+ protected shownOverrideFingerprint: number | undefined;
91
+ protected readonly toDispose = new DisposableCollection();
92
+
93
+ registerCommands(commands: CommandRegistry): void {
94
+ commands.registerCommand(RERUN_AGENTS_MD_MIGRATION_COMMAND, {
95
+ execute: () => this.runMigration(true)
96
+ });
97
+ }
98
+
99
+ onStart(): void {
100
+ // Full configuration reloads publish their completed source maps via this event.
101
+ this.toDispose.push(this.customizationService.onDidChangeCustomAgents(() => this.scheduleOverrideWarning()));
102
+ this.toDispose.push(this.customizationService.onDidChangePromptFragmentCustomization(ids => {
103
+ if (ids.includes(PROJECT_INFO_PROMPT_FRAGMENT_ID)) {
104
+ this.scheduleOverrideWarning();
105
+ }
106
+ }));
107
+ this.runStartupMigration();
108
+ this.toDispose.push(this.workspaceTrustService.onDidChangeWorkspaceTrust(trusted => {
109
+ if (trusted) {
110
+ this.runStartupMigration();
111
+ }
112
+ }));
113
+ Promise.all([
114
+ this.appStateService.reachedState('ready'),
115
+ this.workspaceService.ready,
116
+ this.workspaceTrustService.getWorkspaceTrust()
117
+ ]).then(() => {
118
+ this.overrideWarningEnabled = true;
119
+ this.scheduleOverrideWarning();
120
+ });
121
+ }
122
+
123
+ onStop(): void {
124
+ clearTimeout(this.overrideWarningTimer);
125
+ this.toDispose.dispose();
126
+ }
127
+
128
+ /**
129
+ * `onDidChangeWorkspaceTrust` fires for the *initial* trust resolution as well as for later
130
+ * transitions, and that resolution lands after `onStart` has already started a run and parked it
131
+ * on the same deferred. Both triggers therefore fire on an ordinary trusted startup. The service
132
+ * keeps them from migrating twice; this keeps the second one from reporting the result again.
133
+ *
134
+ * Deliberately an in-flight promise rather than a latch: a workspace that starts untrusted has
135
+ * nothing to migrate, and the run that matters is the one after the user grants trust.
136
+ */
137
+ protected runStartupMigration(): Promise<void> {
138
+ if (!this.startupMigration) {
139
+ this.startupMigration = this.runMigration(false).finally(() => {
140
+ this.startupMigration = undefined;
141
+ });
142
+ }
143
+ return this.startupMigration;
144
+ }
145
+
146
+ /**
147
+ * @param reportEmptyResult whether to tell the user when there was nothing to migrate. Only the
148
+ * command does: on startup the common case is that nothing is pending, and saying so every time
149
+ * would be noise.
150
+ */
151
+ protected async runMigration(reportEmptyResult: boolean): Promise<void> {
152
+ try {
153
+ const reports = await this.migrationService.migrate();
154
+ if (reports.length === 0 && reportEmptyResult && !(await this.workspaceTrustService.getWorkspaceTrust())) {
155
+ // The migration skips untrusted workspaces, so "nothing to migrate" would be misleading.
156
+ this.messageService?.info(nls.localize('theia/ai/core/agentsMd/migrationResult/untrusted',
157
+ 'AGENTS.md migration: the workspace is not trusted, so no project info was migrated.'));
158
+ } else {
159
+ this.showMigrationSummary(reports, reportEmptyResult);
160
+ }
161
+ this.scheduleOverrideWarning();
162
+ } catch (e) {
163
+ this.logger.warn('AGENTS.md migration failed', e);
164
+ }
165
+ }
166
+
167
+ protected scheduleOverrideWarning(): void {
168
+ if (!this.overrideWarningEnabled) {
169
+ // A check is scheduled as soon as it is.
170
+ return;
171
+ }
172
+ clearTimeout(this.overrideWarningTimer);
173
+ this.overrideWarningTimer = setTimeout(() => {
174
+ this.overrideWarningTimer = undefined;
175
+ this.overrideWarningUpdates = this.overrideWarningUpdates.then(async () => {
176
+ // Migration can rename a local source while the initial template scan is still running.
177
+ await this.startupMigration;
178
+ await this.showOverrideWarning();
179
+ }).catch(error => this.logger.warn('Failed to report legacy project-info overrides', error));
180
+ }, this.overrideWarningDelay);
181
+ }
182
+
183
+ protected async showOverrideWarning(): Promise<void> {
184
+ if (!this.messageService || !(await this.workspaceTrustService.getWorkspaceTrust())) {
185
+ return;
186
+ }
187
+ await this.workspaceService.ready;
188
+ const roots = this.workspaceService.tryGetRoots();
189
+ if (roots.length === 0) {
190
+ return;
191
+ }
192
+ const localSources = new Set(roots.map(root => root.resource.resolve(LEGACY_PROJECT_INFO_PATH).toString()));
193
+ const sources = (this.customizationService.getPromptFragmentCustomizationSources?.(PROJECT_INFO_PROMPT_FRAGMENT_ID) ?? [])
194
+ .filter(source => !localSources.has(source.uri.toString()))
195
+ .sort((left, right) => left.uri.toString().localeCompare(right.uri.toString()));
196
+ if (sources.length === 0) {
197
+ this.shownOverrideFingerprint = undefined;
198
+ return;
199
+ }
200
+ // The active status is deliberately not part of it: it depends on which template locations
201
+ // are loaded, so it can differ between startups without anything the user would act on.
202
+ const fingerprint = hash([
203
+ this.workspaceService.workspace?.resource.toString(),
204
+ roots.map(root => root.resource.toString()).sort(),
205
+ sources.map(source => [source.uri.toString(), source.template])
206
+ ]);
207
+ const storageKey = 'ai-core.agentsMd.dismissedProjectInfoOverrides';
208
+ if (this.shownOverrideFingerprint === fingerprint || await this.storageService.getData<number>(storageKey) === fingerprint) {
209
+ return;
210
+ }
211
+ this.shownOverrideFingerprint = fingerprint;
212
+ // Notifications render Markdown and collapse newlines, so each path is a code span in a flat list.
213
+ const paths = sources.map(source => nls.localizeByDefault(
214
+ '{0} ({1})', `\`${source.uri.path.toString()}\``,
215
+ source.active ? nls.localizeByDefault('Active') : nls.localize('theia/ai/core/agentsMd/inactiveOverride', 'Inactive')
216
+ )).join(', ');
217
+ const message = nls.localize(
218
+ 'theia/ai/core/agentsMd/legacyOverrides',
219
+ 'These project-info prompt overrides are not migrated automatically and no longer reach the built-in agents that use AGENTS.md. '
220
+ + 'Move the instructions you want to keep into the relevant project\'s AGENTS.md. '
221
+ + 'They still apply to prompts that reference project-info: {0}', paths
222
+ );
223
+ const openAction = nls.localize('theia/ai/core/agentsMd/openOverrideSources', 'Open Source Files');
224
+ this.messageService.warn(message, { timeout: 0 }, openAction).then(async choice => {
225
+ if (choice === openAction) {
226
+ for (const source of sources) {
227
+ await open(this.openerService, source.uri);
228
+ }
229
+ } else {
230
+ await this.storageService.setData(storageKey, fingerprint);
231
+ }
232
+ }).catch(error => this.logger.warn('Failed to handle legacy project-info override notification', error));
233
+ }
234
+
235
+ protected showMigrationSummary(reports: AgentsMdMigrationReport[], reportEmptyResult: boolean): void {
236
+ if (!this.messageService) {
237
+ return;
238
+ }
239
+ if (reports.length === 0) {
240
+ if (reportEmptyResult) {
241
+ this.messageService.info(nls.localize('theia/ai/core/agentsMd/migrationResult/empty', 'AGENTS.md migration: nothing to migrate.'));
242
+ }
243
+ return;
244
+ }
245
+
246
+ const migrated = reports.filter(report => report.migrated);
247
+ const alreadyPresent = reports.filter(report => report.alreadyPresent).length;
248
+ const failed = reports.filter(report => report.error).length;
249
+ const withPromptSyntax = reports.filter(report => report.containsPromptSyntax).length;
250
+ const notBackedUp = reports.filter(report => !report.error && !report.backedUp).length;
251
+
252
+ let message = nls.localize(
253
+ 'theia/ai/core/agentsMd/migrationResult',
254
+ 'AGENTS.md migration: {0} written, {1} skipped because an AGENTS.md already existed, {2} failed.',
255
+ migrated.length, alreadyPresent, failed
256
+ );
257
+ if (notBackedUp === 0) {
258
+ message += ' ' + nls.localize(
259
+ 'theia/ai/core/agentsMd/migrationResult/backedUp',
260
+ 'The previous project info was kept as `{0}`.',
261
+ LEGACY_PROJECT_INFO_BACKUP_PATH
262
+ );
263
+ } else {
264
+ // Not cosmetic: while the legacy file is in place it overrides the built-in `project-info`
265
+ // fragment, so an AGENTS.md written next to it is never the one used.
266
+ message += ' ' + nls.localize(
267
+ 'theia/ai/core/agentsMd/migrationResult/backupFailed',
268
+ '{0} could not be renamed to `{1}`, so it is still in place and takes precedence over {2}. '
269
+ + 'Rename or remove it to start using {2}.',
270
+ LEGACY_PROJECT_INFO_PATH, LEGACY_PROJECT_INFO_BACKUP_PATH, AGENTS_MD_FILE_NAME
271
+ );
272
+ }
273
+
274
+ const openAction = migrated.length > 0 ? nls.localizeByDefault('Open') : undefined;
275
+ // Keep the result visible until dismissed: it announces a new file in the user's repository.
276
+ this.messageService.info(message, { timeout: 0 }, ...(openAction ? [openAction] : [])).then(choice => {
277
+ // Dismissing resolves to `undefined`, which must not be mistaken for the action when there is none.
278
+ if (openAction && choice === openAction) {
279
+ open(this.openerService, migrated[0].root.resolve(AGENTS_MD_FILE_NAME));
280
+ }
281
+ });
282
+ if (withPromptSyntax > 0) {
283
+ // A separate warning: appended to the summary it was easy to miss.
284
+ this.messageService.warn(nls.localize(
285
+ 'theia/ai/core/agentsMd/migrationResult/promptSyntax',
286
+ '{0} of the migrated files still contain prompt template syntax, which {1} no longer resolves. Review and remove it.',
287
+ withPromptSyntax, AGENTS_MD_FILE_NAME
288
+ ), { timeout: 0 });
289
+ }
290
+ }
291
+ }