@theia/ai-core 1.77.0-next.7 → 1.77.0-next.71
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/agents-md-frontend-application-contribution.d.ts +65 -0
- package/lib/browser/agents-md-frontend-application-contribution.d.ts.map +1 -0
- package/lib/browser/agents-md-frontend-application-contribution.js +269 -0
- package/lib/browser/agents-md-frontend-application-contribution.js.map +1 -0
- package/lib/browser/agents-md-frontend-application-contribution.spec.d.ts +2 -0
- package/lib/browser/agents-md-frontend-application-contribution.spec.d.ts.map +1 -0
- package/lib/browser/agents-md-frontend-application-contribution.spec.js +225 -0
- package/lib/browser/agents-md-frontend-application-contribution.spec.js.map +1 -0
- package/lib/browser/agents-md-migration-service.d.ts +78 -0
- package/lib/browser/agents-md-migration-service.d.ts.map +1 -0
- package/lib/browser/agents-md-migration-service.js +195 -0
- package/lib/browser/agents-md-migration-service.js.map +1 -0
- package/lib/browser/agents-md-migration-service.spec.d.ts +2 -0
- package/lib/browser/agents-md-migration-service.spec.d.ts.map +1 -0
- package/lib/browser/agents-md-migration-service.spec.js +278 -0
- package/lib/browser/agents-md-migration-service.spec.js.map +1 -0
- package/lib/browser/agents-md-prompt-fragment-contribution.d.ts +13 -0
- package/lib/browser/agents-md-prompt-fragment-contribution.d.ts.map +1 -0
- package/lib/browser/agents-md-prompt-fragment-contribution.js +76 -0
- package/lib/browser/agents-md-prompt-fragment-contribution.js.map +1 -0
- package/lib/browser/agents-md-service.d.ts +58 -0
- package/lib/browser/agents-md-service.d.ts.map +1 -0
- package/lib/browser/agents-md-service.js +191 -0
- package/lib/browser/agents-md-service.js.map +1 -0
- package/lib/browser/agents-md-service.spec.d.ts +2 -0
- package/lib/browser/agents-md-service.spec.d.ts.map +1 -0
- package/lib/browser/agents-md-service.spec.js +220 -0
- package/lib/browser/agents-md-service.spec.js.map +1 -0
- package/lib/browser/agents-md-variable-contribution.d.ts +66 -0
- package/lib/browser/agents-md-variable-contribution.d.ts.map +1 -0
- package/lib/browser/agents-md-variable-contribution.js +169 -0
- package/lib/browser/agents-md-variable-contribution.js.map +1 -0
- package/lib/browser/agents-md-variable-contribution.spec.d.ts +2 -0
- package/lib/browser/agents-md-variable-contribution.spec.d.ts.map +1 -0
- package/lib/browser/agents-md-variable-contribution.spec.js +148 -0
- package/lib/browser/agents-md-variable-contribution.spec.js.map +1 -0
- package/lib/browser/ai-core-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-core-frontend-module.js +17 -0
- package/lib/browser/ai-core-frontend-module.js.map +1 -1
- package/lib/browser/file-variable-contribution.d.ts +2 -0
- package/lib/browser/file-variable-contribution.d.ts.map +1 -1
- package/lib/browser/file-variable-contribution.js +8 -23
- package/lib/browser/file-variable-contribution.js.map +1 -1
- package/lib/browser/file-variable-contribution.spec.d.ts +2 -0
- package/lib/browser/file-variable-contribution.spec.d.ts.map +1 -0
- package/lib/browser/file-variable-contribution.spec.js +98 -0
- package/lib/browser/file-variable-contribution.spec.js.map +1 -0
- package/lib/browser/frontend-language-model-service.d.ts.map +1 -1
- package/lib/browser/frontend-language-model-service.js +0 -8
- package/lib/browser/frontend-language-model-service.js.map +1 -1
- package/lib/browser/frontend-prompt-customization-service.d.ts +2 -1
- package/lib/browser/frontend-prompt-customization-service.d.ts.map +1 -1
- package/lib/browser/frontend-prompt-customization-service.js +10 -0
- package/lib/browser/frontend-prompt-customization-service.js.map +1 -1
- package/lib/browser/frontend-prompt-customization-service.spec.js +19 -1
- package/lib/browser/frontend-prompt-customization-service.spec.js.map +1 -1
- package/lib/browser/frontend-variable-service.js +1 -1
- package/lib/browser/frontend-variable-service.js.map +1 -1
- package/lib/browser/index.d.ts +6 -0
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +6 -0
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/prompt-variable-contribution.js +1 -1
- package/lib/browser/prompt-variable-contribution.js.map +1 -1
- package/lib/browser/prompt-variable-contribution.spec.js +2 -1
- package/lib/browser/prompt-variable-contribution.spec.js.map +1 -1
- package/lib/browser/prompttemplate-contribution.js +1 -1
- package/lib/browser/prompttemplate-contribution.js.map +1 -1
- package/lib/browser/reasoning-labels.d.ts +4 -0
- package/lib/browser/reasoning-labels.d.ts.map +1 -0
- package/lib/browser/reasoning-labels.js +41 -0
- package/lib/browser/reasoning-labels.js.map +1 -0
- package/lib/browser/reasoning-labels.spec.d.ts +2 -0
- package/lib/browser/reasoning-labels.spec.d.ts.map +1 -0
- package/lib/browser/reasoning-labels.spec.js +28 -0
- package/lib/browser/reasoning-labels.spec.js.map +1 -0
- package/lib/browser/skill-service.spec.js +2 -1
- package/lib/browser/skill-service.spec.js.map +1 -1
- package/lib/browser/skills-variable-contribution.spec.js +2 -1
- package/lib/browser/skills-variable-contribution.spec.js.map +1 -1
- package/lib/browser/theia-variable-contribution.js +3 -3
- package/lib/browser/theia-variable-contribution.js.map +1 -1
- package/lib/browser/workspace-relative-path-resolver.d.ts +20 -0
- package/lib/browser/workspace-relative-path-resolver.d.ts.map +1 -0
- package/lib/browser/workspace-relative-path-resolver.js +78 -0
- package/lib/browser/workspace-relative-path-resolver.js.map +1 -0
- package/lib/common/agents-md.d.ts +20 -0
- package/lib/common/agents-md.d.ts.map +1 -0
- package/lib/common/agents-md.js +38 -0
- package/lib/common/agents-md.js.map +1 -0
- package/lib/common/ai-core-preferences.d.ts.map +1 -1
- package/lib/common/ai-core-preferences.js +6 -3
- package/lib/common/ai-core-preferences.js.map +1 -1
- package/lib/common/ai-variable-resource.js +3 -3
- package/lib/common/ai-variable-resource.js.map +1 -1
- package/lib/common/capability-variable-contribution.js +1 -1
- package/lib/common/capability-variable-contribution.js.map +1 -1
- package/lib/common/index.d.ts +2 -0
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js +2 -0
- package/lib/common/index.js.map +1 -1
- package/lib/common/language-model-service.d.ts.map +1 -1
- package/lib/common/language-model-service.js +7 -0
- package/lib/common/language-model-service.js.map +1 -1
- package/lib/common/language-model-service.spec.js +21 -0
- package/lib/common/language-model-service.spec.js.map +1 -1
- package/lib/common/language-model.d.ts +10 -3
- package/lib/common/language-model.d.ts.map +1 -1
- package/lib/common/language-model.js +17 -4
- package/lib/common/language-model.js.map +1 -1
- package/lib/common/language-model.spec.js +18 -0
- package/lib/common/language-model.spec.js.map +1 -1
- package/lib/common/prompt-service.d.ts +12 -0
- package/lib/common/prompt-service.d.ts.map +1 -1
- package/lib/common/prompt-service.js.map +1 -1
- package/lib/common/prompt-service.spec.js +2 -1
- package/lib/common/prompt-service.spec.js.map +1 -1
- package/lib/common/tool-call-execution.d.ts +89 -0
- package/lib/common/tool-call-execution.d.ts.map +1 -0
- package/lib/common/tool-call-execution.js +67 -0
- package/lib/common/tool-call-execution.js.map +1 -0
- package/lib/common/tool-call-execution.spec.d.ts +2 -0
- package/lib/common/tool-call-execution.spec.d.ts.map +1 -0
- package/lib/common/tool-call-execution.spec.js +114 -0
- package/lib/common/tool-call-execution.spec.js.map +1 -0
- package/lib/common/tool-request-parameters.spec.d.ts +2 -0
- package/lib/common/tool-request-parameters.spec.d.ts.map +1 -0
- package/lib/common/tool-request-parameters.spec.js +62 -0
- package/lib/common/tool-request-parameters.spec.js.map +1 -0
- package/lib/common/variable-service.spec.js +2 -1
- package/lib/common/variable-service.spec.js.map +1 -1
- package/lib/node/ai-core-backend-module.d.ts.map +1 -1
- package/lib/node/ai-core-backend-module.js +2 -0
- package/lib/node/ai-core-backend-module.js.map +1 -1
- package/lib/node/model-snapshot-store.js +2 -2
- package/lib/node/model-snapshot-store.js.map +1 -1
- package/lib/node/proxy-util.js +2 -1
- package/lib/node/proxy-util.js.map +1 -1
- package/package.json +9 -9
- package/src/browser/agents-md-frontend-application-contribution.spec.ts +244 -0
- package/src/browser/agents-md-frontend-application-contribution.ts +291 -0
- package/src/browser/agents-md-migration-service.spec.ts +338 -0
- package/src/browser/agents-md-migration-service.ts +226 -0
- package/src/browser/agents-md-prompt-fragment-contribution.ts +73 -0
- package/src/browser/agents-md-service.spec.ts +258 -0
- package/src/browser/agents-md-service.ts +212 -0
- package/src/browser/agents-md-variable-contribution.spec.ts +180 -0
- package/src/browser/agents-md-variable-contribution.ts +182 -0
- package/src/browser/ai-core-frontend-module.ts +19 -0
- package/src/browser/file-variable-contribution.spec.ts +107 -0
- package/src/browser/file-variable-contribution.ts +8 -27
- package/src/browser/frontend-language-model-service.ts +1 -10
- package/src/browser/frontend-prompt-customization-service.spec.ts +24 -0
- package/src/browser/frontend-prompt-customization-service.ts +13 -1
- package/src/browser/index.ts +6 -0
- package/src/browser/reasoning-labels.spec.ts +28 -0
- package/src/browser/reasoning-labels.ts +41 -0
- package/src/browser/theia-variable-contribution.ts +3 -3
- package/src/browser/workspace-relative-path-resolver.ts +71 -0
- package/src/common/agents-md.ts +37 -0
- package/src/common/ai-core-preferences.ts +6 -3
- package/src/common/ai-variable-resource.ts +1 -1
- package/src/common/index.ts +2 -0
- package/src/common/language-model-service.spec.ts +23 -1
- package/src/common/language-model-service.ts +11 -1
- package/src/common/language-model.spec.ts +21 -0
- package/src/common/language-model.ts +26 -6
- package/src/common/prompt-service.ts +14 -0
- package/src/common/tool-call-execution.spec.ts +162 -0
- package/src/common/tool-call-execution.ts +161 -0
- package/src/common/tool-request-parameters.spec.ts +81 -0
- package/src/node/ai-core-backend-module.ts +5 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
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
|
+
|
|
20
|
+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
|
21
|
+
// Guarded: another spec in the same mocha process may already have set it, and `set` throws on a
|
|
22
|
+
// second call.
|
|
23
|
+
try {
|
|
24
|
+
FrontendApplicationConfigProvider.get();
|
|
25
|
+
} catch {
|
|
26
|
+
FrontendApplicationConfigProvider.set({});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
import { expect } from 'chai';
|
|
30
|
+
import * as sinon from 'sinon';
|
|
31
|
+
import { ILogger, Logger, URI } from '@theia/core';
|
|
32
|
+
import { PreferenceScope } from '@theia/core/lib/common/preferences';
|
|
33
|
+
import { AISettings } from '../common';
|
|
34
|
+
import { AISettingsServiceImpl } from './ai-settings-service';
|
|
35
|
+
import { AgentsMdMigrationService, LEGACY_PROJECT_INFO_PATH } from './agents-md-migration-service';
|
|
36
|
+
|
|
37
|
+
disableJSDOM();
|
|
38
|
+
|
|
39
|
+
describe('AgentsMdMigrationService', () => {
|
|
40
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
41
|
+
let fileServiceMock: any;
|
|
42
|
+
let workspaceServiceMock: any;
|
|
43
|
+
let trustServiceMock: any;
|
|
44
|
+
let configurationServiceMock: any;
|
|
45
|
+
/** User-scope `ai-features.agentSettings`. */
|
|
46
|
+
let settings: AISettings;
|
|
47
|
+
/** Workspace-scope `ai-features.agentSettings`, i.e. what `.theia/settings.json` holds. */
|
|
48
|
+
let workspaceScopedSettings: AISettings | undefined;
|
|
49
|
+
let trusted: boolean;
|
|
50
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
51
|
+
|
|
52
|
+
const legacyPath = `/ws/${LEGACY_PROJECT_INFO_PATH}`;
|
|
53
|
+
|
|
54
|
+
/** What a trust-aware, scope-merging read returns: workspace scope wins, and is hidden while untrusted. */
|
|
55
|
+
function effectiveSettings(): AISettings {
|
|
56
|
+
return { ...settings, ...(trusted ? workspaceScopedSettings : undefined) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createService(): AgentsMdMigrationService {
|
|
60
|
+
const service = new AgentsMdMigrationService();
|
|
61
|
+
(service as unknown as { fileService: unknown }).fileService = fileServiceMock;
|
|
62
|
+
(service as unknown as { workspaceService: unknown }).workspaceService = workspaceServiceMock;
|
|
63
|
+
(service as unknown as { workspaceTrustService: unknown }).workspaceTrustService = trustServiceMock;
|
|
64
|
+
(service as unknown as { aiConfigurationService: unknown }).aiConfigurationService = configurationServiceMock;
|
|
65
|
+
const loggerMock: ILogger = sinon.createStubInstance(Logger);
|
|
66
|
+
(service as unknown as { logger: unknown }).logger = loggerMock;
|
|
67
|
+
return service;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function existing(...paths: string[]): void {
|
|
71
|
+
fileServiceMock.exists.callsFake((uri: URI) => Promise.resolve(paths.includes(uri.path.toString())));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createdContent(): string {
|
|
75
|
+
return fileServiceMock.createFile.firstCall.args[1].toString();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
settings = {};
|
|
80
|
+
workspaceScopedSettings = undefined;
|
|
81
|
+
fileServiceMock = {
|
|
82
|
+
exists: sinon.stub().resolves(false),
|
|
83
|
+
read: sinon.stub().resolves({ value: 'project conventions' }),
|
|
84
|
+
createFile: sinon.stub().resolves(undefined),
|
|
85
|
+
move: sinon.stub().resolves(undefined)
|
|
86
|
+
};
|
|
87
|
+
workspaceServiceMock = {
|
|
88
|
+
tryGetRoots: sinon.stub().returns([{ resource: new URI('file:///ws'), name: 'ws' }])
|
|
89
|
+
};
|
|
90
|
+
trusted = true;
|
|
91
|
+
trustServiceMock = { getWorkspaceTrust: sinon.stub().callsFake(() => Promise.resolve(trusted)) };
|
|
92
|
+
// Models the two properties that make the real service dangerous to call carelessly: reads
|
|
93
|
+
// are merged across scopes and suppress workspace scope while untrusted, and `update` writes
|
|
94
|
+
// to the narrowest scope the key is already defined in. A flat store cannot express either,
|
|
95
|
+
// which is why an earlier version of this suite could not fail on a scope mix-up.
|
|
96
|
+
configurationServiceMock = {
|
|
97
|
+
ready: Promise.resolve(),
|
|
98
|
+
get: sinon.stub().callsFake(() => effectiveSettings()),
|
|
99
|
+
inspect: sinon.stub().callsFake(() => ({
|
|
100
|
+
globalValue: settings,
|
|
101
|
+
workspaceValue: trusted ? workspaceScopedSettings : undefined,
|
|
102
|
+
value: effectiveSettings()
|
|
103
|
+
})),
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
105
|
+
update: sinon.stub().callsFake((_name: string, value: any) => {
|
|
106
|
+
if (workspaceScopedSettings) {
|
|
107
|
+
workspaceScopedSettings = value;
|
|
108
|
+
} else {
|
|
109
|
+
settings = value;
|
|
110
|
+
}
|
|
111
|
+
return Promise.resolve();
|
|
112
|
+
}),
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
114
|
+
set: sinon.stub().callsFake((_name: string, value: any, scope: PreferenceScope) => {
|
|
115
|
+
if (scope === PreferenceScope.Workspace) {
|
|
116
|
+
workspaceScopedSettings = value;
|
|
117
|
+
} else {
|
|
118
|
+
settings = value;
|
|
119
|
+
}
|
|
120
|
+
return Promise.resolve();
|
|
121
|
+
})
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
afterEach(() => sinon.restore());
|
|
126
|
+
|
|
127
|
+
describe('file migration', () => {
|
|
128
|
+
it('reports nothing when there is no legacy file', async () => {
|
|
129
|
+
expect(await createService().migrate()).to.be.empty;
|
|
130
|
+
expect(fileServiceMock.createFile.called).to.be.false;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('writes AGENTS.md from the legacy file and backs the legacy file up', async () => {
|
|
134
|
+
existing(legacyPath);
|
|
135
|
+
const reports = await createService().migrate();
|
|
136
|
+
|
|
137
|
+
expect(reports).to.have.lengthOf(1);
|
|
138
|
+
expect(reports[0].migrated).to.be.true;
|
|
139
|
+
expect(reports[0].backedUp).to.be.true;
|
|
140
|
+
expect(fileServiceMock.createFile.firstCall.args[0].path.toString()).to.equal('/ws/AGENTS.md');
|
|
141
|
+
expect(createdContent()).to.equal('project conventions');
|
|
142
|
+
expect(fileServiceMock.move.firstCall.args[1].path.toString()).to.equal(`${legacyPath}.bak`);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('never overwrites an existing AGENTS.md, but still retires the legacy file', async () => {
|
|
146
|
+
existing(legacyPath, '/ws/AGENTS.md');
|
|
147
|
+
fileServiceMock.read.callsFake((uri: URI) => Promise.resolve({
|
|
148
|
+
value: uri.path.toString() === '/ws/AGENTS.md' ? 'own conventions' : 'project conventions'
|
|
149
|
+
}));
|
|
150
|
+
const reports = await createService().migrate();
|
|
151
|
+
|
|
152
|
+
expect(reports[0].migrated).to.be.false;
|
|
153
|
+
expect(reports[0].alreadyPresent).to.be.true;
|
|
154
|
+
expect(reports[0].backedUp).to.be.true;
|
|
155
|
+
expect(fileServiceMock.createFile.called).to.be.false;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('finishes a run that was cut off after writing AGENTS.md', async () => {
|
|
159
|
+
existing(legacyPath, '/ws/AGENTS.md');
|
|
160
|
+
const reports = await createService().migrate();
|
|
161
|
+
|
|
162
|
+
expect(reports[0].migrated).to.be.true;
|
|
163
|
+
expect(reports[0].alreadyPresent).to.be.false;
|
|
164
|
+
expect(reports[0].backedUp).to.be.true;
|
|
165
|
+
expect(fileServiceMock.createFile.called).to.be.false;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('is idempotent: a second run finds nothing once the legacy file is gone', async () => {
|
|
169
|
+
existing(legacyPath);
|
|
170
|
+
const service = createService();
|
|
171
|
+
await service.migrate();
|
|
172
|
+
|
|
173
|
+
existing('/ws/AGENTS.md', `${legacyPath}.bak`);
|
|
174
|
+
expect(await service.migrate()).to.be.empty;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('flags prompt template syntax that AGENTS.md no longer resolves', async () => {
|
|
178
|
+
existing(legacyPath);
|
|
179
|
+
fileServiceMock.read.resolves({ value: 'Read files with ~{getFileContent} and see {{contextFiles}}.' });
|
|
180
|
+
|
|
181
|
+
const reports = await createService().migrate();
|
|
182
|
+
|
|
183
|
+
expect(reports[0].containsPromptSyntax).to.be.true;
|
|
184
|
+
// The content is still carried over verbatim; nothing is silently stripped.
|
|
185
|
+
expect(createdContent()).to.equal('Read files with ~{getFileContent} and see {{contextFiles}}.');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('does not flag plain content', async () => {
|
|
189
|
+
existing(legacyPath);
|
|
190
|
+
const reports = await createService().migrate();
|
|
191
|
+
|
|
192
|
+
expect(reports[0].containsPromptSyntax).to.be.false;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('reports the failure and leaves the legacy file alone when the write fails', async () => {
|
|
196
|
+
existing(legacyPath);
|
|
197
|
+
fileServiceMock.createFile.rejects(new Error('read-only'));
|
|
198
|
+
|
|
199
|
+
const reports = await createService().migrate();
|
|
200
|
+
|
|
201
|
+
expect(reports[0].error).to.contain('read-only');
|
|
202
|
+
expect(reports[0].migrated).to.be.false;
|
|
203
|
+
expect(fileServiceMock.move.called).to.be.false;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('migrates every workspace root that has a legacy file', async () => {
|
|
207
|
+
workspaceServiceMock.tryGetRoots.returns([
|
|
208
|
+
{ resource: new URI('file:///a'), name: 'a' },
|
|
209
|
+
{ resource: new URI('file:///b'), name: 'b' }
|
|
210
|
+
]);
|
|
211
|
+
existing(`/a/${LEGACY_PROJECT_INFO_PATH}`, `/b/${LEGACY_PROJECT_INFO_PATH}`);
|
|
212
|
+
|
|
213
|
+
const reports = await createService().migrate();
|
|
214
|
+
|
|
215
|
+
expect(reports.map(report => report.migrated)).to.deep.equal([true, true]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('coalesces concurrent calls into a single migration', async () => {
|
|
219
|
+
// Startup and the initial workspace-trust resolution both trigger a migration. Without
|
|
220
|
+
// coalescing both reach `createFile`, and the loser throws because the write passes no
|
|
221
|
+
// `overwrite`, producing a phantom failure for a migration that in fact succeeded.
|
|
222
|
+
existing(legacyPath);
|
|
223
|
+
const service = createService();
|
|
224
|
+
|
|
225
|
+
const [first, second] = await Promise.all([service.migrate(), service.migrate()]);
|
|
226
|
+
|
|
227
|
+
expect(fileServiceMock.createFile.callCount).to.equal(1);
|
|
228
|
+
expect(fileServiceMock.move.callCount).to.equal(1);
|
|
229
|
+
expect(second).to.equal(first);
|
|
230
|
+
expect(first[0].error).to.be.undefined;
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('starts a fresh migration once the previous one has settled', async () => {
|
|
234
|
+
existing(legacyPath);
|
|
235
|
+
const service = createService();
|
|
236
|
+
await service.migrate();
|
|
237
|
+
|
|
238
|
+
await service.migrate();
|
|
239
|
+
|
|
240
|
+
expect(fileServiceMock.createFile.callCount).to.equal(2);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('reports the backup as not done when the rename fails', async () => {
|
|
244
|
+
existing(legacyPath);
|
|
245
|
+
fileServiceMock.move.rejects(new Error('read-only'));
|
|
246
|
+
|
|
247
|
+
const reports = await createService().migrate();
|
|
248
|
+
|
|
249
|
+
expect(reports[0].migrated).to.be.true;
|
|
250
|
+
expect(reports[0].backedUp).to.be.false;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('writes nothing into an untrusted workspace', async () => {
|
|
254
|
+
existing(legacyPath);
|
|
255
|
+
trustServiceMock.getWorkspaceTrust.resolves(false);
|
|
256
|
+
|
|
257
|
+
expect(await createService().migrate()).to.be.empty;
|
|
258
|
+
expect(fileServiceMock.createFile.called).to.be.false;
|
|
259
|
+
expect(fileServiceMock.move.called).to.be.false;
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('agent settings migration', () => {
|
|
264
|
+
it('carries the settings over to the new agent id', async () => {
|
|
265
|
+
settings = { ProjectInfo: { enable: true, languageModelRequirements: [{ purpose: 'chat', identifier: 'my/model' }] } };
|
|
266
|
+
|
|
267
|
+
await createService().migrate();
|
|
268
|
+
|
|
269
|
+
expect(settings.ProjectInfo).to.be.undefined;
|
|
270
|
+
expect(settings.AgentsMd).to.deep.equal({ enable: true, languageModelRequirements: [{ purpose: 'chat', identifier: 'my/model' }] });
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('renames the prompt ids in selectedVariants', async () => {
|
|
274
|
+
settings = { ProjectInfo: { selectedVariants: { 'project-info-system': 'my-variant', 'other-prompt': 'keep' } } };
|
|
275
|
+
|
|
276
|
+
await createService().migrate();
|
|
277
|
+
|
|
278
|
+
expect(settings.AgentsMd.selectedVariants).to.deep.equal({ 'agents-md-system': 'my-variant', 'other-prompt': 'keep' });
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('leaves other agents untouched', async () => {
|
|
282
|
+
settings = { ProjectInfo: { enable: false }, Coder: { enable: true } };
|
|
283
|
+
|
|
284
|
+
await createService().migrate();
|
|
285
|
+
|
|
286
|
+
expect(settings.Coder).to.deep.equal({ enable: true });
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('does nothing when the new id is already configured', async () => {
|
|
290
|
+
settings = { ProjectInfo: { enable: false }, AgentsMd: { enable: true } };
|
|
291
|
+
|
|
292
|
+
await createService().migrate();
|
|
293
|
+
|
|
294
|
+
expect(configurationServiceMock.set.called).to.be.false;
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('does nothing when there are no legacy settings', async () => {
|
|
298
|
+
await createService().migrate();
|
|
299
|
+
|
|
300
|
+
expect(configurationServiceMock.set.called).to.be.false;
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('runs untrusted, and addresses user scope on both the read and the write', async () => {
|
|
304
|
+
settings = { ProjectInfo: { enable: true } };
|
|
305
|
+
trusted = false;
|
|
306
|
+
|
|
307
|
+
await createService().migrate();
|
|
308
|
+
|
|
309
|
+
expect(settings.AgentsMd).to.deep.equal({ enable: true });
|
|
310
|
+
expect(configurationServiceMock.set.firstCall.args[0]).to.equal(AISettingsServiceImpl.PREFERENCE_NAME);
|
|
311
|
+
expect(configurationServiceMock.set.firstCall.args[2]).to.equal(PreferenceScope.User);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('never rewrites workspace-scoped settings from a user-scope read while untrusted', async () => {
|
|
315
|
+
// The scenario the scope mix-up destroyed: a shared `.theia/settings.json` holding other
|
|
316
|
+
// agents, invisible to the read because the workspace is untrusted.
|
|
317
|
+
settings = { ProjectInfo: { enable: true } };
|
|
318
|
+
workspaceScopedSettings = { Coder: { enable: true }, Universal: { enable: false } };
|
|
319
|
+
trusted = false;
|
|
320
|
+
|
|
321
|
+
await createService().migrate();
|
|
322
|
+
|
|
323
|
+
expect(workspaceScopedSettings).to.deep.equal({ Coder: { enable: true }, Universal: { enable: false } });
|
|
324
|
+
expect(settings.AgentsMd).to.deep.equal({ enable: true });
|
|
325
|
+
expect(settings.ProjectInfo).to.be.undefined;
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('leaves a workspace-scoped entry alone rather than promoting it into user scope', async () => {
|
|
329
|
+
workspaceScopedSettings = { Coder: { enable: true } };
|
|
330
|
+
settings = { ProjectInfo: { enable: true } };
|
|
331
|
+
|
|
332
|
+
await createService().migrate();
|
|
333
|
+
|
|
334
|
+
expect(settings.Coder).to.be.undefined;
|
|
335
|
+
expect(workspaceScopedSettings).to.deep.equal({ Coder: { enable: true } });
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
});
|