@theia/ai-core 1.71.0-next.8 → 1.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/ai-activation-service.d.ts +9 -0
- package/lib/browser/ai-activation-service.d.ts.map +1 -1
- package/lib/browser/ai-activation-service.js +5 -0
- package/lib/browser/ai-activation-service.js.map +1 -1
- package/lib/browser/ai-core-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-core-frontend-module.js +5 -1
- package/lib/browser/ai-core-frontend-module.js.map +1 -1
- package/lib/browser/ai-settings-service.d.ts +2 -0
- package/lib/browser/ai-settings-service.d.ts.map +1 -1
- package/lib/browser/ai-settings-service.js +8 -1
- package/lib/browser/ai-settings-service.js.map +1 -1
- package/lib/browser/frontend-language-model-alias-registry.d.ts +2 -0
- package/lib/browser/frontend-language-model-alias-registry.d.ts.map +1 -1
- package/lib/browser/frontend-language-model-alias-registry.js +18 -9
- package/lib/browser/frontend-language-model-alias-registry.js.map +1 -1
- package/lib/browser/frontend-language-model-service.d.ts +6 -4
- package/lib/browser/frontend-language-model-service.d.ts.map +1 -1
- package/lib/browser/frontend-language-model-service.js +34 -16
- package/lib/browser/frontend-language-model-service.js.map +1 -1
- package/lib/browser/frontend-prompt-customization-service.d.ts +7 -0
- package/lib/browser/frontend-prompt-customization-service.d.ts.map +1 -1
- package/lib/browser/frontend-prompt-customization-service.js +39 -13
- package/lib/browser/frontend-prompt-customization-service.js.map +1 -1
- package/lib/browser/product-name-variable-contribution.d.ts +9 -0
- package/lib/browser/product-name-variable-contribution.d.ts.map +1 -0
- package/lib/browser/product-name-variable-contribution.js +49 -0
- package/lib/browser/product-name-variable-contribution.js.map +1 -0
- package/lib/browser/product-name-variable-contribution.spec.d.ts +2 -0
- package/lib/browser/product-name-variable-contribution.spec.d.ts.map +1 -0
- package/lib/browser/product-name-variable-contribution.spec.js +50 -0
- package/lib/browser/product-name-variable-contribution.spec.js.map +1 -0
- package/lib/browser/skill-prompt-coordinator.d.ts +1 -1
- package/lib/browser/skill-prompt-coordinator.d.ts.map +1 -1
- package/lib/browser/skill-prompt-coordinator.js +3 -1
- package/lib/browser/skill-prompt-coordinator.js.map +1 -1
- package/lib/browser/skill-service.d.ts +5 -0
- package/lib/browser/skill-service.d.ts.map +1 -1
- package/lib/browser/skill-service.js +6 -0
- package/lib/browser/skill-service.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/trust-aware-preference-reader.d.ts +38 -0
- package/lib/browser/trust-aware-preference-reader.d.ts.map +1 -0
- package/lib/browser/trust-aware-preference-reader.js +114 -0
- package/lib/browser/trust-aware-preference-reader.js.map +1 -0
- package/lib/browser/trust-aware-preference-reader.spec.d.ts +2 -0
- package/lib/browser/trust-aware-preference-reader.spec.d.ts.map +1 -0
- package/lib/browser/trust-aware-preference-reader.spec.js +251 -0
- package/lib/browser/trust-aware-preference-reader.spec.js.map +1 -0
- package/lib/common/ai-core-preferences.d.ts +5 -7
- package/lib/common/ai-core-preferences.d.ts.map +1 -1
- package/lib/common/ai-core-preferences.js +34 -37
- package/lib/common/ai-core-preferences.js.map +1 -1
- package/lib/common/language-model.d.ts +25 -4
- package/lib/common/language-model.d.ts.map +1 -1
- package/lib/common/language-model.js.map +1 -1
- package/lib/common/settings-service.d.ts +7 -1
- package/lib/common/settings-service.d.ts.map +1 -1
- package/lib/node/backend-language-model-registry.d.ts.map +1 -1
- package/lib/node/backend-language-model-registry.js +1 -0
- package/lib/node/backend-language-model-registry.js.map +1 -1
- package/package.json +12 -12
- package/src/browser/ai-activation-service.ts +16 -0
- package/src/browser/ai-core-frontend-module.ts +6 -1
- package/src/browser/ai-settings-service.ts +10 -1
- package/src/browser/frontend-language-model-alias-registry.ts +17 -9
- package/src/browser/frontend-language-model-service.ts +36 -20
- package/src/browser/frontend-prompt-customization-service.ts +38 -15
- package/src/browser/product-name-variable-contribution.spec.ts +68 -0
- package/src/browser/product-name-variable-contribution.ts +47 -0
- package/src/browser/skill-prompt-coordinator.ts +4 -1
- package/src/browser/skill-service.ts +10 -0
- package/src/browser/skills-variable-contribution.spec.ts +2 -1
- package/src/browser/trust-aware-preference-reader.spec.ts +304 -0
- package/src/browser/trust-aware-preference-reader.ts +108 -0
- package/src/common/ai-core-preferences.ts +38 -41
- package/src/common/language-model.ts +28 -4
- package/src/common/settings-service.ts +7 -1
- package/src/node/backend-language-model-registry.ts +1 -0
|
@@ -18,6 +18,7 @@ import { DisposableCollection, URI, Event, Emitter, nls } from '@theia/core';
|
|
|
18
18
|
import { OpenerService } from '@theia/core/lib/browser';
|
|
19
19
|
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
|
20
20
|
import { PromptFragmentCustomizationService, CustomAgentDescription, CustomizedPromptFragment, CommandPromptFragmentMetadata } from '../common';
|
|
21
|
+
import { ConfigurableInMemoryResources } from '../common/configurable-in-memory-resources';
|
|
21
22
|
import { BinaryBuffer } from '@theia/core/lib/common/buffer';
|
|
22
23
|
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
23
24
|
import { FileChangesEvent } from '@theia/filesystem/lib/common/files';
|
|
@@ -132,6 +133,9 @@ export class DefaultPromptFragmentCustomizationService implements PromptFragment
|
|
|
132
133
|
@inject(OpenerService)
|
|
133
134
|
protected readonly openerService: OpenerService;
|
|
134
135
|
|
|
136
|
+
@inject(ConfigurableInMemoryResources)
|
|
137
|
+
protected readonly inMemoryResources: ConfigurableInMemoryResources;
|
|
138
|
+
|
|
135
139
|
/** Stores URI strings of template files from directories currently being monitored for changes. */
|
|
136
140
|
protected trackedTemplateURIs = new Set<string>();
|
|
137
141
|
|
|
@@ -852,11 +856,38 @@ export class DefaultPromptFragmentCustomizationService implements PromptFragment
|
|
|
852
856
|
*/
|
|
853
857
|
protected async editTemplate(id: string, defaultContent?: string): Promise<void> {
|
|
854
858
|
const editorUri = await this.getTemplateURI(id);
|
|
855
|
-
if (
|
|
856
|
-
await this.
|
|
859
|
+
if (await this.fileService.exists(editorUri)) {
|
|
860
|
+
const openHandler = await this.openerService.getOpener(editorUri);
|
|
861
|
+
openHandler.open(editorUri);
|
|
862
|
+
} else {
|
|
863
|
+
await this.openInMemoryTemplate(editorUri, defaultContent ?? '');
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Opens an in-memory resource with the given content, without creating a file on disk.
|
|
869
|
+
* The file is only created when the user saves in the editor.
|
|
870
|
+
*/
|
|
871
|
+
protected async openInMemoryTemplate(templateUri: URI, defaultContent: string): Promise<void> {
|
|
872
|
+
try {
|
|
873
|
+
this.inMemoryResources.resolve(templateUri);
|
|
874
|
+
} catch {
|
|
875
|
+
const resource = this.inMemoryResources.add(templateUri, {
|
|
876
|
+
contents: defaultContent,
|
|
877
|
+
initiallyDirty: false,
|
|
878
|
+
onSave: async (contents: string) => {
|
|
879
|
+
const dirUri = templateUri.parent;
|
|
880
|
+
if (!(await this.fileService.exists(dirUri))) {
|
|
881
|
+
await this.fileService.createFolder(dirUri);
|
|
882
|
+
}
|
|
883
|
+
await this.fileService.createFile(templateUri, BinaryBuffer.fromString(contents), { overwrite: true });
|
|
884
|
+
resource.dispose();
|
|
885
|
+
}
|
|
886
|
+
});
|
|
857
887
|
}
|
|
858
|
-
|
|
859
|
-
openHandler.
|
|
888
|
+
|
|
889
|
+
const openHandler = await this.openerService.getOpener(templateUri);
|
|
890
|
+
openHandler.open(templateUri);
|
|
860
891
|
}
|
|
861
892
|
|
|
862
893
|
async removePromptFragmentCustomization(id: string, customizationId: string): Promise<void> {
|
|
@@ -978,18 +1009,10 @@ export class DefaultPromptFragmentCustomizationService implements PromptFragment
|
|
|
978
1009
|
const openHandler = await this.openerService.getOpener(uri);
|
|
979
1010
|
openHandler.open(uri);
|
|
980
1011
|
} else {
|
|
981
|
-
//
|
|
982
|
-
//
|
|
1012
|
+
// Open the built-in content without creating a file on disk.
|
|
1013
|
+
// The file will only be created when the user saves.
|
|
983
1014
|
const templateUri = await this.getTemplateURI(id);
|
|
984
|
-
|
|
985
|
-
// If template doesn't exist, create it with default content
|
|
986
|
-
if (!(await this.fileService.exists(templateUri))) {
|
|
987
|
-
await this.fileService.createFile(templateUri, BinaryBuffer.fromString(defaultContent));
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
// Open the template in the editor
|
|
991
|
-
const openHandler = await this.openerService.getOpener(templateUri);
|
|
992
|
-
openHandler.open(templateUri);
|
|
1015
|
+
await this.openInMemoryTemplate(templateUri, defaultContent);
|
|
993
1016
|
}
|
|
994
1017
|
}
|
|
995
1018
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH and others.
|
|
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
|
+
|
|
19
|
+
let disableJSDOM = enableJSDOM();
|
|
20
|
+
|
|
21
|
+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
|
22
|
+
FrontendApplicationConfigProvider.set({ applicationName: 'Test IDE' });
|
|
23
|
+
|
|
24
|
+
import 'reflect-metadata';
|
|
25
|
+
|
|
26
|
+
import { expect } from 'chai';
|
|
27
|
+
import { ProductNameVariableContribution, PRODUCT_NAME_VARIABLE } from './product-name-variable-contribution';
|
|
28
|
+
|
|
29
|
+
disableJSDOM();
|
|
30
|
+
|
|
31
|
+
describe('ProductNameVariableContribution', () => {
|
|
32
|
+
before(() => {
|
|
33
|
+
disableJSDOM = enableJSDOM();
|
|
34
|
+
FrontendApplicationConfigProvider.set({ applicationName: 'Test IDE' });
|
|
35
|
+
});
|
|
36
|
+
after(() => disableJSDOM());
|
|
37
|
+
|
|
38
|
+
let contribution: ProductNameVariableContribution;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
contribution = new ProductNameVariableContribution();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should resolve to the configured application name', async () => {
|
|
45
|
+
const result = await contribution.resolve(
|
|
46
|
+
{ variable: PRODUCT_NAME_VARIABLE },
|
|
47
|
+
{}
|
|
48
|
+
);
|
|
49
|
+
expect(result).to.not.be.undefined;
|
|
50
|
+
expect(result!.value).to.equal('Test IDE');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should return undefined for unknown variables', async () => {
|
|
54
|
+
const result = await contribution.resolve(
|
|
55
|
+
{ variable: { id: 'other', name: 'other', description: 'other' } },
|
|
56
|
+
{}
|
|
57
|
+
);
|
|
58
|
+
expect(result).to.be.undefined;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should return a positive priority from canResolve', async () => {
|
|
62
|
+
const priority = await contribution.canResolve(
|
|
63
|
+
{ variable: PRODUCT_NAME_VARIABLE },
|
|
64
|
+
{}
|
|
65
|
+
);
|
|
66
|
+
expect(priority).to.equal(1);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH and others.
|
|
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 { MaybePromise, nls } from '@theia/core';
|
|
18
|
+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
|
19
|
+
import { injectable } from '@theia/core/shared/inversify';
|
|
20
|
+
import { AIVariable, AIVariableContribution, AIVariableContext, AIVariableResolutionRequest, AIVariableResolver, AIVariableService, ResolvedAIVariable } from '../common';
|
|
21
|
+
|
|
22
|
+
export const PRODUCT_NAME_VARIABLE: AIVariable = {
|
|
23
|
+
id: 'product-name-provider',
|
|
24
|
+
name: 'productName',
|
|
25
|
+
description: nls.localize('theia/ai/core/productNameVariable/description', 'The name of the product/application the user is working with'),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
@injectable()
|
|
29
|
+
export class ProductNameVariableContribution implements AIVariableContribution, AIVariableResolver {
|
|
30
|
+
registerVariables(service: AIVariableService): void {
|
|
31
|
+
service.registerResolver(PRODUCT_NAME_VARIABLE, this);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
canResolve(request: AIVariableResolutionRequest, context: AIVariableContext): MaybePromise<number> {
|
|
35
|
+
if (request.variable.name === PRODUCT_NAME_VARIABLE.name) {
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
return -1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async resolve(request: AIVariableResolutionRequest, context: AIVariableContext): Promise<ResolvedAIVariable | undefined> {
|
|
42
|
+
if (request.variable.name === PRODUCT_NAME_VARIABLE.name) {
|
|
43
|
+
return { variable: request.variable, value: FrontendApplicationConfigProvider.get().applicationName };
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -30,7 +30,10 @@ export class SkillPromptCoordinator implements FrontendApplicationContribution {
|
|
|
30
30
|
|
|
31
31
|
protected registeredSkillCommands = new Set<string>();
|
|
32
32
|
|
|
33
|
-
onStart(): void {
|
|
33
|
+
async onStart(): Promise<void> {
|
|
34
|
+
// Wait for skills to be loaded before registering commands
|
|
35
|
+
await this.skillService.ready;
|
|
36
|
+
|
|
34
37
|
// Register initial skills
|
|
35
38
|
this.updateSkillCommands();
|
|
36
39
|
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';
|
|
18
|
+
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
18
19
|
import { DisposableCollection, Emitter, Event, ILogger, URI } from '@theia/core';
|
|
19
20
|
import { Path } from '@theia/core/lib/common/path';
|
|
20
21
|
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
|
@@ -37,6 +38,9 @@ export interface SkillService {
|
|
|
37
38
|
|
|
38
39
|
/** Event fired when skills change */
|
|
39
40
|
readonly onSkillsChanged: Event<void>;
|
|
41
|
+
|
|
42
|
+
/** Promise that resolves when initial skill loading is complete */
|
|
43
|
+
readonly ready: Promise<void>;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
@injectable()
|
|
@@ -68,6 +72,11 @@ export class DefaultSkillService implements SkillService {
|
|
|
68
72
|
|
|
69
73
|
protected updateDebounceTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
70
74
|
|
|
75
|
+
protected _ready = new Deferred<void>();
|
|
76
|
+
get ready(): Promise<void> {
|
|
77
|
+
return this._ready.promise;
|
|
78
|
+
}
|
|
79
|
+
|
|
71
80
|
@postConstruct()
|
|
72
81
|
protected init(): void {
|
|
73
82
|
this.fileService.onDidFilesChange(async (event: FileChangesEvent) => {
|
|
@@ -113,6 +122,7 @@ export class DefaultSkillService implements SkillService {
|
|
|
113
122
|
// Wait for workspace to be ready before initial update
|
|
114
123
|
this.workspaceService.ready.then(() => {
|
|
115
124
|
this.update().then(() => {
|
|
125
|
+
this._ready.resolve();
|
|
116
126
|
// Only after initial update, start listening for changes
|
|
117
127
|
this.lastSkillDirectoriesValue = JSON.stringify(this.preferences[PREFERENCE_NAME_SKILL_DIRECTORIES]);
|
|
118
128
|
|
|
@@ -52,7 +52,8 @@ describe('SkillsVariableContribution', () => {
|
|
|
52
52
|
skillService = {
|
|
53
53
|
getSkills: sinon.stub(),
|
|
54
54
|
getSkill: sinon.stub(),
|
|
55
|
-
onSkillsChanged: sinon.stub() as unknown as typeof skillService.onSkillsChanged
|
|
55
|
+
onSkillsChanged: sinon.stub() as unknown as typeof skillService.onSkillsChanged,
|
|
56
|
+
ready: sinon.stub() as unknown as typeof skillService.ready
|
|
56
57
|
};
|
|
57
58
|
|
|
58
59
|
container.bind(SkillService).toConstantValue(skillService as unknown as SkillService);
|
|
@@ -0,0 +1,304 @@
|
|
|
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
|
+
FrontendApplicationConfigProvider.set({});
|
|
21
|
+
|
|
22
|
+
import { expect } from 'chai';
|
|
23
|
+
import { Container } from '@theia/core/shared/inversify';
|
|
24
|
+
import { Emitter, Event } from '@theia/core';
|
|
25
|
+
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
26
|
+
import { PreferenceService } from '@theia/core/lib/common/preferences';
|
|
27
|
+
import { WorkspaceTrustService } from '@theia/workspace/lib/browser/workspace-trust-service';
|
|
28
|
+
import { TrustAwarePreferenceReader } from './trust-aware-preference-reader';
|
|
29
|
+
|
|
30
|
+
disableJSDOM();
|
|
31
|
+
|
|
32
|
+
interface InspectResult<T> {
|
|
33
|
+
defaultValue?: T;
|
|
34
|
+
globalValue?: T;
|
|
35
|
+
workspaceValue?: T;
|
|
36
|
+
workspaceFolderValue?: T;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class StubPreferenceService {
|
|
40
|
+
inspectResult: InspectResult<unknown> | undefined;
|
|
41
|
+
effectiveValue: unknown;
|
|
42
|
+
|
|
43
|
+
get<T>(_preferenceName: string, fallback?: T, _resourceUri?: string): T | undefined {
|
|
44
|
+
return ((this.effectiveValue as T | undefined) ?? fallback);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
inspect<T>(_preferenceName: string, _resourceUri?: string): InspectResult<T> | undefined {
|
|
48
|
+
return this.inspectResult as InspectResult<T> | undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class StubWorkspaceTrustService {
|
|
53
|
+
readonly trustDeferred = new Deferred<boolean>();
|
|
54
|
+
protected readonly emitter = new Emitter<boolean>();
|
|
55
|
+
readonly onDidChangeWorkspaceTrust: Event<boolean> = this.emitter.event;
|
|
56
|
+
|
|
57
|
+
getWorkspaceTrust(): Promise<boolean> {
|
|
58
|
+
return this.trustDeferred.promise;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fireTrustChange(trusted: boolean): void {
|
|
62
|
+
this.emitter.fire(trusted);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const PREFERENCE_NAME = 'some.preference';
|
|
67
|
+
|
|
68
|
+
describe('TrustAwarePreferenceReader', () => {
|
|
69
|
+
let preferences: StubPreferenceService;
|
|
70
|
+
let trust: StubWorkspaceTrustService;
|
|
71
|
+
let reader: TrustAwarePreferenceReader;
|
|
72
|
+
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
preferences = new StubPreferenceService();
|
|
75
|
+
trust = new StubWorkspaceTrustService();
|
|
76
|
+
|
|
77
|
+
const container = new Container();
|
|
78
|
+
container.bind(PreferenceService).toConstantValue(preferences as unknown as PreferenceService);
|
|
79
|
+
container.bind(WorkspaceTrustService).toConstantValue(trust as unknown as WorkspaceTrustService);
|
|
80
|
+
container.bind(TrustAwarePreferenceReader).toSelf().inSingletonScope();
|
|
81
|
+
|
|
82
|
+
reader = container.get(TrustAwarePreferenceReader);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('fail-closed default', () => {
|
|
86
|
+
it('returns only user/default/fallback before ready resolves', () => {
|
|
87
|
+
preferences.effectiveValue = 'workspace';
|
|
88
|
+
preferences.inspectResult = {
|
|
89
|
+
defaultValue: 'default',
|
|
90
|
+
globalValue: 'user',
|
|
91
|
+
workspaceValue: 'workspace'
|
|
92
|
+
};
|
|
93
|
+
const value = reader.get<string>(PREFERENCE_NAME, 'fallback');
|
|
94
|
+
expect(value).to.equal('user');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns defaultValue when only workspace value exists before ready', () => {
|
|
98
|
+
preferences.effectiveValue = 'workspace';
|
|
99
|
+
preferences.inspectResult = {
|
|
100
|
+
defaultValue: 'default',
|
|
101
|
+
workspaceValue: 'workspace'
|
|
102
|
+
};
|
|
103
|
+
const value = reader.get<string>(PREFERENCE_NAME, 'fallback');
|
|
104
|
+
expect(value).to.equal('default');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('returns fallback when nothing is set before ready', () => {
|
|
108
|
+
preferences.effectiveValue = undefined;
|
|
109
|
+
preferences.inspectResult = {};
|
|
110
|
+
const value = reader.get<string>(PREFERENCE_NAME, 'fallback');
|
|
111
|
+
expect(value).to.equal('fallback');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('after ready resolves with trusted = true', () => {
|
|
116
|
+
beforeEach(async () => {
|
|
117
|
+
trust.trustDeferred.resolve(true);
|
|
118
|
+
await reader.ready;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('delegates to preferences.get and returns the workspace value', () => {
|
|
122
|
+
preferences.effectiveValue = 'workspace';
|
|
123
|
+
preferences.inspectResult = {
|
|
124
|
+
defaultValue: 'default',
|
|
125
|
+
globalValue: 'user',
|
|
126
|
+
workspaceValue: 'workspace'
|
|
127
|
+
};
|
|
128
|
+
const value = reader.get<string>(PREFERENCE_NAME, 'fallback');
|
|
129
|
+
expect(value).to.equal('workspace');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('after ready resolves with trusted = false', () => {
|
|
134
|
+
beforeEach(async () => {
|
|
135
|
+
trust.trustDeferred.resolve(false);
|
|
136
|
+
await reader.ready;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('returns globalValue when both global and workspace values exist', () => {
|
|
140
|
+
preferences.effectiveValue = 'workspace';
|
|
141
|
+
preferences.inspectResult = {
|
|
142
|
+
defaultValue: 'default',
|
|
143
|
+
globalValue: 'user',
|
|
144
|
+
workspaceValue: 'workspace'
|
|
145
|
+
};
|
|
146
|
+
const value = reader.get<string>(PREFERENCE_NAME, 'fallback');
|
|
147
|
+
expect(value).to.equal('user');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('falls back to defaultValue when only workspace value exists', () => {
|
|
151
|
+
preferences.effectiveValue = 'workspace';
|
|
152
|
+
preferences.inspectResult = {
|
|
153
|
+
defaultValue: 'default',
|
|
154
|
+
workspaceValue: 'workspace'
|
|
155
|
+
};
|
|
156
|
+
const value = reader.get<string>(PREFERENCE_NAME, 'fallback');
|
|
157
|
+
expect(value).to.equal('default');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('returns the supplied fallback when nothing is set', () => {
|
|
161
|
+
preferences.effectiveValue = undefined;
|
|
162
|
+
preferences.inspectResult = {};
|
|
163
|
+
const value = reader.get<string>(PREFERENCE_NAME, 'fallback');
|
|
164
|
+
expect(value).to.equal('fallback');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('onDidChangeTrust', () => {
|
|
169
|
+
beforeEach(async () => {
|
|
170
|
+
trust.trustDeferred.resolve(false);
|
|
171
|
+
await reader.ready;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('fires with the new value when the underlying service emits a change', () => {
|
|
175
|
+
const received: boolean[] = [];
|
|
176
|
+
reader.onDidChangeTrust(value => received.push(value));
|
|
177
|
+
|
|
178
|
+
trust.fireTrustChange(true);
|
|
179
|
+
expect(received).to.deep.equal([true]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('does not fire when the value is unchanged', () => {
|
|
183
|
+
const received: boolean[] = [];
|
|
184
|
+
reader.onDidChangeTrust(value => received.push(value));
|
|
185
|
+
|
|
186
|
+
trust.fireTrustChange(false);
|
|
187
|
+
expect(received).to.deep.equal([]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('has updated the cached trusted flag by the time listeners run', () => {
|
|
191
|
+
preferences.effectiveValue = 'workspace';
|
|
192
|
+
preferences.inspectResult = {
|
|
193
|
+
defaultValue: 'default',
|
|
194
|
+
globalValue: 'user',
|
|
195
|
+
workspaceValue: 'workspace'
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
let observedDuringListener: string | undefined;
|
|
199
|
+
reader.onDidChangeTrust(() => {
|
|
200
|
+
observedDuringListener = reader.get<string>(PREFERENCE_NAME, 'fallback');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
trust.fireTrustChange(true);
|
|
204
|
+
expect(observedDuringListener).to.equal('workspace');
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('ready', () => {
|
|
209
|
+
it('does not resolve until the initial getWorkspaceTrust promise resolves', async () => {
|
|
210
|
+
let resolved = false;
|
|
211
|
+
reader.ready.then(() => { resolved = true; });
|
|
212
|
+
|
|
213
|
+
// Allow microtasks to settle without resolving the deferred.
|
|
214
|
+
await Promise.resolve();
|
|
215
|
+
expect(resolved).to.equal(false);
|
|
216
|
+
|
|
217
|
+
trust.trustDeferred.resolve(true);
|
|
218
|
+
await reader.ready;
|
|
219
|
+
expect(resolved).to.equal(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('resolves via an early change event when it arrives before the initial promise', async () => {
|
|
223
|
+
const received: boolean[] = [];
|
|
224
|
+
reader.onDidChangeTrust(value => received.push(value));
|
|
225
|
+
|
|
226
|
+
trust.fireTrustChange(true);
|
|
227
|
+
await reader.ready;
|
|
228
|
+
|
|
229
|
+
// The first signal acts as the initial resolution and must not
|
|
230
|
+
// be reported as a change event.
|
|
231
|
+
expect(received).to.deep.equal([]);
|
|
232
|
+
|
|
233
|
+
preferences.effectiveValue = 'workspace';
|
|
234
|
+
preferences.inspectResult = {
|
|
235
|
+
defaultValue: 'default',
|
|
236
|
+
globalValue: 'user',
|
|
237
|
+
workspaceValue: 'workspace'
|
|
238
|
+
};
|
|
239
|
+
expect(reader.get<string>(PREFERENCE_NAME, 'fallback')).to.equal('workspace');
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('race between initial promise and change event', () => {
|
|
244
|
+
it('ignores a stale getWorkspaceTrust resolution after a change event has initialised the reader', async () => {
|
|
245
|
+
const received: boolean[] = [];
|
|
246
|
+
reader.onDidChangeTrust(value => received.push(value));
|
|
247
|
+
|
|
248
|
+
// Change event arrives first and initialises the reader.
|
|
249
|
+
trust.fireTrustChange(false);
|
|
250
|
+
// The (now stale) initial promise resolves to a different value.
|
|
251
|
+
trust.trustDeferred.resolve(true);
|
|
252
|
+
|
|
253
|
+
await reader.ready;
|
|
254
|
+
// Allow the stale .then callback to run.
|
|
255
|
+
await Promise.resolve();
|
|
256
|
+
await Promise.resolve();
|
|
257
|
+
|
|
258
|
+
preferences.effectiveValue = 'workspace';
|
|
259
|
+
preferences.inspectResult = {
|
|
260
|
+
defaultValue: 'default',
|
|
261
|
+
globalValue: 'user',
|
|
262
|
+
workspaceValue: 'workspace'
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// Cached trust must still reflect the change event (false), not
|
|
266
|
+
// the stale promise resolution (true).
|
|
267
|
+
expect(reader.get<string>(PREFERENCE_NAME, 'fallback')).to.equal('user');
|
|
268
|
+
// No change event should have been fired for the initial value
|
|
269
|
+
// and none for the stale resolution either.
|
|
270
|
+
expect(received).to.deep.equal([]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('ignores a stale getWorkspaceTrust rejection after a change event has initialised the reader', async () => {
|
|
274
|
+
const received: boolean[] = [];
|
|
275
|
+
reader.onDidChangeTrust(value => received.push(value));
|
|
276
|
+
|
|
277
|
+
// Avoid an unhandled rejection warning for the (now stale) initial promise.
|
|
278
|
+
trust.getWorkspaceTrust().catch(() => { /* expected */ });
|
|
279
|
+
|
|
280
|
+
// Change event arrives first and initialises the reader.
|
|
281
|
+
trust.fireTrustChange(true);
|
|
282
|
+
// The (now stale) initial promise rejects.
|
|
283
|
+
trust.trustDeferred.reject(new Error('stale failure'));
|
|
284
|
+
|
|
285
|
+
// ready must resolve (not reject) because the event already initialised it.
|
|
286
|
+
await reader.ready;
|
|
287
|
+
// Allow the stale rejection callback to run.
|
|
288
|
+
await Promise.resolve();
|
|
289
|
+
await Promise.resolve();
|
|
290
|
+
|
|
291
|
+
preferences.effectiveValue = 'workspace';
|
|
292
|
+
preferences.inspectResult = {
|
|
293
|
+
defaultValue: 'default',
|
|
294
|
+
globalValue: 'user',
|
|
295
|
+
workspaceValue: 'workspace'
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// Cached trust must reflect the change event (true).
|
|
299
|
+
expect(reader.get<string>(PREFERENCE_NAME, 'fallback')).to.equal('workspace');
|
|
300
|
+
// No change event should have been fired for the initial value.
|
|
301
|
+
expect(received).to.deep.equal([]);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
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, postConstruct } from '@theia/core/shared/inversify';
|
|
18
|
+
import { JSONValue } from '@theia/core/shared/@lumino/coreutils';
|
|
19
|
+
import { Emitter, Event } from '@theia/core';
|
|
20
|
+
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
21
|
+
import { PreferenceService } from '@theia/core/lib/common/preferences';
|
|
22
|
+
import { WorkspaceTrustService } from '@theia/workspace/lib/browser/workspace-trust-service';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Helper for reading AI preferences that must ignore workspace/folder scopes
|
|
26
|
+
* when the current workspace is not trusted.
|
|
27
|
+
*
|
|
28
|
+
* Writes should continue to go through `PreferenceService` directly; trust only
|
|
29
|
+
* affects reads.
|
|
30
|
+
*/
|
|
31
|
+
@injectable()
|
|
32
|
+
export class TrustAwarePreferenceReader {
|
|
33
|
+
|
|
34
|
+
@inject(PreferenceService)
|
|
35
|
+
protected readonly preferences: PreferenceService;
|
|
36
|
+
|
|
37
|
+
@inject(WorkspaceTrustService)
|
|
38
|
+
protected readonly trust: WorkspaceTrustService;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Cached trust flag updated via `onDidChangeWorkspaceTrust`. Defaults to
|
|
42
|
+
* `false` (fail closed): until the initial trust state has resolved, reads
|
|
43
|
+
* return only user/default scope values. Asynchronous callers should
|
|
44
|
+
* `await ready` before their first read.
|
|
45
|
+
*/
|
|
46
|
+
protected trusted = false;
|
|
47
|
+
|
|
48
|
+
protected readonly _ready = new Deferred<void>();
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolves once the initial workspace trust state has been resolved.
|
|
52
|
+
* Callers that need a deterministic trust state before their first read can
|
|
53
|
+
* await this promise.
|
|
54
|
+
*/
|
|
55
|
+
get ready(): Promise<void> {
|
|
56
|
+
return this._ready.promise;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
protected readonly onDidChangeTrustEmitter = new Emitter<boolean>();
|
|
60
|
+
readonly onDidChangeTrust: Event<boolean> = this.onDidChangeTrustEmitter.event;
|
|
61
|
+
|
|
62
|
+
@postConstruct()
|
|
63
|
+
protected init(): void {
|
|
64
|
+
let initialized = false;
|
|
65
|
+
|
|
66
|
+
this.trust.onDidChangeWorkspaceTrust(t => {
|
|
67
|
+
if (!initialized) {
|
|
68
|
+
initialized = true;
|
|
69
|
+
this.trusted = t;
|
|
70
|
+
this._ready.resolve();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (this.trusted === t) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.trusted = t;
|
|
77
|
+
this.onDidChangeTrustEmitter.fire(t);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
this.trust.getWorkspaceTrust().then(t => {
|
|
81
|
+
if (initialized) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
initialized = true;
|
|
85
|
+
this.trusted = t;
|
|
86
|
+
this._ready.resolve();
|
|
87
|
+
}, err => {
|
|
88
|
+
if (initialized) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
initialized = true;
|
|
92
|
+
this._ready.reject(err);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Reads the preference, ignoring workspace/folder scopes when the workspace
|
|
98
|
+
* is untrusted. Returns `globalValue ?? defaultValue ?? fallback`.
|
|
99
|
+
*/
|
|
100
|
+
get<T>(preferenceName: string, fallback?: T, resourceUri?: string): T | undefined {
|
|
101
|
+
if (this.trusted) {
|
|
102
|
+
return this.preferences.get<T>(preferenceName, fallback, resourceUri);
|
|
103
|
+
}
|
|
104
|
+
const inspection = this.preferences.inspect<JSONValue>(preferenceName, resourceUri);
|
|
105
|
+
const value = inspection?.globalValue ?? inspection?.defaultValue;
|
|
106
|
+
return (value as T | undefined) ?? fallback;
|
|
107
|
+
}
|
|
108
|
+
}
|