@theia/ai-core-ui 1.75.0-next.8 → 1.75.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-configuration/ai-configuration-category-registry.d.ts +29 -0
- package/lib/browser/ai-configuration/ai-configuration-category-registry.d.ts.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-category-registry.js +83 -0
- package/lib/browser/ai-configuration/ai-configuration-category-registry.js.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-category-registry.spec.d.ts +2 -0
- package/lib/browser/ai-configuration/ai-configuration-category-registry.spec.d.ts.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-category-registry.spec.js +122 -0
- package/lib/browser/ai-configuration/ai-configuration-category-registry.spec.js.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-category.d.ts +204 -0
- package/lib/browser/ai-configuration/ai-configuration-category.d.ts.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-category.js +86 -0
- package/lib/browser/ai-configuration/ai-configuration-category.js.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-selection-model.d.ts +15 -0
- package/lib/browser/ai-configuration/ai-configuration-selection-model.d.ts.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-selection-model.js +44 -0
- package/lib/browser/ai-configuration/ai-configuration-selection-model.js.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-setting-commands.d.ts +48 -0
- package/lib/browser/ai-configuration/ai-configuration-setting-commands.d.ts.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-setting-commands.js +98 -0
- package/lib/browser/ai-configuration/ai-configuration-setting-commands.js.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-setting-commands.spec.d.ts +2 -0
- package/lib/browser/ai-configuration/ai-configuration-setting-commands.spec.d.ts.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-setting-commands.spec.js +70 -0
- package/lib/browser/ai-configuration/ai-configuration-setting-commands.spec.js.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-components.spec.d.ts +2 -0
- package/lib/browser/ai-configuration/components/ai-configuration-components.spec.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-components.spec.js +596 -0
- package/lib/browser/ai-configuration/components/ai-configuration-components.spec.js.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-controls.d.ts +118 -0
- package/lib/browser/ai-configuration/components/ai-configuration-controls.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-controls.js +191 -0
- package/lib/browser/ai-configuration/components/ai-configuration-controls.js.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-item-row.d.ts +57 -0
- package/lib/browser/ai-configuration/components/ai-configuration-item-row.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-item-row.js +48 -0
- package/lib/browser/ai-configuration/components/ai-configuration-item-row.js.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-origin-badge.d.ts +49 -0
- package/lib/browser/ai-configuration/components/ai-configuration-origin-badge.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-origin-badge.js +79 -0
- package/lib/browser/ai-configuration/components/ai-configuration-origin-badge.js.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-primitives.d.ts +161 -0
- package/lib/browser/ai-configuration/components/ai-configuration-primitives.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-primitives.js +131 -0
- package/lib/browser/ai-configuration/components/ai-configuration-primitives.js.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-setting-row.d.ts +54 -0
- package/lib/browser/ai-configuration/components/ai-configuration-setting-row.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-setting-row.js +77 -0
- package/lib/browser/ai-configuration/components/ai-configuration-setting-row.js.map +1 -0
- package/lib/browser/ai-configuration/components/ai-settings-row-service.d.ts +194 -0
- package/lib/browser/ai-configuration/components/ai-settings-row-service.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/ai-settings-row-service.js +352 -0
- package/lib/browser/ai-configuration/components/ai-settings-row-service.js.map +1 -0
- package/lib/browser/ai-configuration/components/ai-settings-row-service.spec.d.ts +2 -0
- package/lib/browser/ai-configuration/components/ai-settings-row-service.spec.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/ai-settings-row-service.spec.js +233 -0
- package/lib/browser/ai-configuration/components/ai-settings-row-service.spec.js.map +1 -0
- package/lib/browser/ai-configuration/components/ai-settings-row.d.ts +50 -0
- package/lib/browser/ai-configuration/components/ai-settings-row.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/ai-settings-row.js +85 -0
- package/lib/browser/ai-configuration/components/ai-settings-row.js.map +1 -0
- package/lib/browser/ai-configuration/components/collapsible-list.d.ts +65 -0
- package/lib/browser/ai-configuration/components/collapsible-list.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/collapsible-list.js +82 -0
- package/lib/browser/ai-configuration/components/collapsible-list.js.map +1 -0
- package/lib/browser/ai-configuration/components/prompt-customization-dialogs.d.ts +35 -0
- package/lib/browser/ai-configuration/components/prompt-customization-dialogs.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/prompt-customization-dialogs.js +83 -0
- package/lib/browser/ai-configuration/components/prompt-customization-dialogs.js.map +1 -0
- package/lib/browser/ai-configuration/components/variant-set-card.d.ts +25 -0
- package/lib/browser/ai-configuration/components/variant-set-card.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/variant-set-card.js +133 -0
- package/lib/browser/ai-configuration/components/variant-set-card.js.map +1 -0
- package/lib/browser/ai-configuration/renderers/collection-category-renderer.d.ts +44 -0
- package/lib/browser/ai-configuration/renderers/collection-category-renderer.d.ts.map +1 -0
- package/lib/browser/ai-configuration/renderers/collection-category-renderer.js +106 -0
- package/lib/browser/ai-configuration/renderers/collection-category-renderer.js.map +1 -0
- package/lib/browser/ai-configuration/renderers/collection-category-renderer.spec.d.ts +2 -0
- package/lib/browser/ai-configuration/renderers/collection-category-renderer.spec.d.ts.map +1 -0
- package/lib/browser/ai-configuration/renderers/collection-category-renderer.spec.js +120 -0
- package/lib/browser/ai-configuration/renderers/collection-category-renderer.spec.js.map +1 -0
- package/lib/browser/ai-configuration/renderers/single-page-category-renderer.d.ts +15 -0
- package/lib/browser/ai-configuration/renderers/single-page-category-renderer.d.ts.map +1 -0
- package/lib/browser/ai-configuration/renderers/single-page-category-renderer.js +20 -0
- package/lib/browser/ai-configuration/renderers/single-page-category-renderer.js.map +1 -0
- package/lib/browser/ai-core-ui-frontend-module.d.ts +1 -0
- package/lib/browser/ai-core-ui-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-core-ui-frontend-module.js +11 -0
- package/lib/browser/ai-core-ui-frontend-module.js.map +1 -1
- package/package.json +6 -5
- package/src/browser/ai-configuration/ai-configuration-category-registry.spec.ts +137 -0
- package/src/browser/ai-configuration/ai-configuration-category-registry.ts +79 -0
- package/src/browser/ai-configuration/ai-configuration-category.ts +246 -0
- package/src/browser/ai-configuration/ai-configuration-selection-model.ts +42 -0
- package/src/browser/ai-configuration/ai-configuration-setting-commands.spec.ts +82 -0
- package/src/browser/ai-configuration/ai-configuration-setting-commands.ts +104 -0
- package/src/browser/ai-configuration/components/ai-configuration-components.spec.ts +655 -0
- package/src/browser/ai-configuration/components/ai-configuration-controls.tsx +382 -0
- package/src/browser/ai-configuration/components/ai-configuration-item-row.tsx +127 -0
- package/src/browser/ai-configuration/components/ai-configuration-origin-badge.tsx +114 -0
- package/src/browser/ai-configuration/components/ai-configuration-primitives.tsx +359 -0
- package/src/browser/ai-configuration/components/ai-configuration-setting-row.tsx +154 -0
- package/src/browser/ai-configuration/components/ai-settings-row-service.spec.ts +275 -0
- package/src/browser/ai-configuration/components/ai-settings-row-service.ts +401 -0
- package/src/browser/ai-configuration/components/ai-settings-row.tsx +176 -0
- package/src/browser/ai-configuration/components/collapsible-list.tsx +205 -0
- package/src/browser/ai-configuration/components/prompt-customization-dialogs.ts +89 -0
- package/src/browser/ai-configuration/components/variant-set-card.tsx +200 -0
- package/src/browser/ai-configuration/renderers/collection-category-renderer.spec.ts +126 -0
- package/src/browser/ai-configuration/renderers/collection-category-renderer.tsx +164 -0
- package/src/browser/ai-configuration/renderers/single-page-category-renderer.tsx +41 -0
- package/src/browser/ai-core-ui-frontend-module.ts +14 -1
- package/src/browser/style/ai-configuration-components.css +1410 -0
- package/src/browser/style/index.css +17 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource 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
|
+
let disableJSDOM = enableJSDOM();
|
|
19
|
+
|
|
20
|
+
import { CommandService, PreferenceInspection, PreferenceScope } from '@theia/core/lib/common';
|
|
21
|
+
import { PreferenceDataProperty, PreferenceSchemaService } from '@theia/core/lib/common/preferences/preference-schema';
|
|
22
|
+
import { PreferencesCommands } from '@theia/preferences/lib/browser/util/preference-types';
|
|
23
|
+
import { AiConfigurationService } from '@theia/ai-core/lib/common/ai-configuration-service';
|
|
24
|
+
import { expect } from 'chai';
|
|
25
|
+
import { AiSettingsRowService } from './ai-settings-row-service';
|
|
26
|
+
|
|
27
|
+
disableJSDOM();
|
|
28
|
+
|
|
29
|
+
interface RecordedSet {
|
|
30
|
+
preferenceId: string;
|
|
31
|
+
value: unknown;
|
|
32
|
+
scope: PreferenceScope | undefined;
|
|
33
|
+
resourceUri: string | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class FakeAiConfigurationService {
|
|
37
|
+
inspection: Partial<PreferenceInspection<unknown>> = {};
|
|
38
|
+
readonly sets: RecordedSet[] = [];
|
|
39
|
+
|
|
40
|
+
inspect(): PreferenceInspection<unknown> | undefined {
|
|
41
|
+
return {
|
|
42
|
+
preferenceName: 'test',
|
|
43
|
+
defaultValue: undefined,
|
|
44
|
+
globalValue: undefined,
|
|
45
|
+
workspaceValue: undefined,
|
|
46
|
+
workspaceFolderValue: undefined,
|
|
47
|
+
value: undefined,
|
|
48
|
+
...this.inspection
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
set(preferenceId: string, value: unknown, scope?: PreferenceScope, resourceUri?: string): Promise<void> {
|
|
53
|
+
this.sets.push({ preferenceId, value, scope, resourceUri });
|
|
54
|
+
return Promise.resolve();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createService(inspection: Partial<PreferenceInspection<unknown>>): { service: AiSettingsRowService; preferences: FakeAiConfigurationService } {
|
|
59
|
+
const preferences = new FakeAiConfigurationService();
|
|
60
|
+
preferences.inspection = inspection;
|
|
61
|
+
const service = new AiSettingsRowService();
|
|
62
|
+
(service as unknown as { aiConfigurationService: AiConfigurationService }).aiConfigurationService = preferences as unknown as AiConfigurationService;
|
|
63
|
+
return { service, preferences };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createServiceWithSchema(property: PreferenceDataProperty | undefined): AiSettingsRowService {
|
|
67
|
+
const service = new AiSettingsRowService();
|
|
68
|
+
const schemaService: Partial<PreferenceSchemaService> = {
|
|
69
|
+
getSchemaProperty: () => property
|
|
70
|
+
};
|
|
71
|
+
(service as unknown as { schemaService: PreferenceSchemaService }).schemaService = schemaService as PreferenceSchemaService;
|
|
72
|
+
return service;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe('AiSettingsRowService', () => {
|
|
76
|
+
|
|
77
|
+
before(() => disableJSDOM = enableJSDOM());
|
|
78
|
+
after(() => disableJSDOM());
|
|
79
|
+
|
|
80
|
+
describe('inspect', () => {
|
|
81
|
+
it('resolves the user-scope value falling back to the default', () => {
|
|
82
|
+
const { service } = createService({ defaultValue: 'd', globalValue: undefined });
|
|
83
|
+
const result = service.inspect('pref', 'user');
|
|
84
|
+
expect(result.value).to.equal('d');
|
|
85
|
+
expect(result.scopeValue).to.equal(undefined);
|
|
86
|
+
expect(result.modified).to.equal(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('reports a user-scope override as modified', () => {
|
|
90
|
+
const { service } = createService({ defaultValue: 'd', globalValue: 'u' });
|
|
91
|
+
const result = service.inspect('pref', 'user');
|
|
92
|
+
expect(result.value).to.equal('u');
|
|
93
|
+
expect(result.scopeValue).to.equal('u');
|
|
94
|
+
expect(result.modified).to.equal(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('resolves the workspace value falling back through user then default', () => {
|
|
98
|
+
const { service } = createService({ defaultValue: 'd', globalValue: 'u' });
|
|
99
|
+
const workspace = service.inspect('pref', 'workspace');
|
|
100
|
+
expect(workspace.value).to.equal('u');
|
|
101
|
+
expect(workspace.scopeValue).to.equal(undefined);
|
|
102
|
+
expect(workspace.modified).to.equal(false);
|
|
103
|
+
|
|
104
|
+
const { service: service2 } = createService({ defaultValue: 'd', globalValue: 'u', workspaceValue: 'w' });
|
|
105
|
+
const workspace2 = service2.inspect('pref', 'workspace');
|
|
106
|
+
expect(workspace2.value).to.equal('w');
|
|
107
|
+
expect(workspace2.modified).to.equal(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('resolves the folder value falling back through workspace, user then default', () => {
|
|
111
|
+
const { service } = createService({ defaultValue: 'd', globalValue: 'u', workspaceValue: 'w' });
|
|
112
|
+
const folder = service.inspect('pref', 'folder');
|
|
113
|
+
expect(folder.value).to.equal('w');
|
|
114
|
+
expect(folder.modified).to.equal(false);
|
|
115
|
+
|
|
116
|
+
const { service: service2 } = createService({ defaultValue: 'd', workspaceFolderValue: 'f' });
|
|
117
|
+
const folder2 = service2.inspect('pref', 'folder');
|
|
118
|
+
expect(folder2.value).to.equal('f');
|
|
119
|
+
expect(folder2.modified).to.equal(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('treats a value of `false` as an explicit override (not inherited)', () => {
|
|
123
|
+
const { service } = createService({ defaultValue: true, globalValue: false });
|
|
124
|
+
const result = service.inspect('pref', 'user');
|
|
125
|
+
expect(result.value).to.equal(false);
|
|
126
|
+
expect(result.modified).to.equal(true);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('controlFor', () => {
|
|
131
|
+
it('maps primitive schema types to their inline controls', () => {
|
|
132
|
+
expect(createServiceWithSchema({ type: 'boolean' }).controlFor('pref').type).to.equal('boolean');
|
|
133
|
+
expect(createServiceWithSchema({ type: 'number' }).controlFor('pref').type).to.equal('number');
|
|
134
|
+
expect(createServiceWithSchema({ type: 'integer' }).controlFor('pref').type).to.equal('number');
|
|
135
|
+
expect(createServiceWithSchema({ type: 'string' }).controlFor('pref').type).to.equal('string');
|
|
136
|
+
expect(createServiceWithSchema(undefined).controlFor('pref').type).to.equal('string');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('marks only an integer schema as whole-number, so a number keeps what the user typed', () => {
|
|
140
|
+
// Both render the stepper; only the integer one may round on commit.
|
|
141
|
+
expect(createServiceWithSchema({ type: 'integer' }).controlFor('pref')).to.deep.include({ type: 'number', integer: true });
|
|
142
|
+
expect(createServiceWithSchema({ type: 'number' }).controlFor('pref')).to.not.have.property('integer');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('maps an enum to a select regardless of its base type', () => {
|
|
146
|
+
expect(createServiceWithSchema({ type: 'string', enum: ['a', 'b'] }).controlFor('pref').type).to.equal('select');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('masks secret string preferences (API keys/tokens) with a password input', () => {
|
|
150
|
+
const control = createServiceWithSchema({ type: 'string' }).controlFor('ai-features.anthropic.apiKey');
|
|
151
|
+
expect(control).to.deep.include({ type: 'string', password: true });
|
|
152
|
+
expect(createServiceWithSchema({ type: 'string' }).controlFor('ai-features.anthropic.model'))
|
|
153
|
+
.to.deep.include({ type: 'string', password: false });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('renders the inline list editor for string arrays', () => {
|
|
157
|
+
expect(createServiceWithSchema({ type: 'array', items: { type: 'string' } }).controlFor('pref').type).to.equal('array');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('defers object and non-string-array preferences to the Settings UI via a json control', () => {
|
|
161
|
+
expect(createServiceWithSchema({ type: 'object' }).controlFor('pref').type).to.equal('json');
|
|
162
|
+
expect(createServiceWithSchema({ type: 'array', items: { type: 'object' } }).controlFor('pref').type).to.equal('json');
|
|
163
|
+
expect(createServiceWithSchema({ type: 'array', items: { type: 'number' } }).controlFor('pref').type).to.equal('json');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('enumOptions', () => {
|
|
168
|
+
it('uses the value as the label and enumDescriptions as the option description (never the description as the label)', () => {
|
|
169
|
+
const options = createServiceWithSchema({
|
|
170
|
+
type: 'string',
|
|
171
|
+
enum: ['default', 'enabled'],
|
|
172
|
+
enumDescriptions: ['Use the default', 'Force enabled']
|
|
173
|
+
}).enumOptions('pref');
|
|
174
|
+
expect(options).to.deep.equal([
|
|
175
|
+
{ value: 'default', label: 'default', description: 'Use the default' },
|
|
176
|
+
{ value: 'enabled', label: 'enabled', description: 'Force enabled' }
|
|
177
|
+
]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('prefers an explicit enumItemLabels entry for the label', () => {
|
|
181
|
+
const options = createServiceWithSchema({
|
|
182
|
+
type: 'string',
|
|
183
|
+
enum: ['a'],
|
|
184
|
+
enumItemLabels: ['Option A'],
|
|
185
|
+
enumDescriptions: ['desc']
|
|
186
|
+
}).enumOptions('pref');
|
|
187
|
+
expect(options[0]).to.deep.equal({ value: 'a', label: 'Option A', description: 'desc' });
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('returns an empty list when the property declares no enum', () => {
|
|
191
|
+
expect(createServiceWithSchema({ type: 'string' }).enumOptions('pref')).to.deep.equal([]);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('modelProviderLabel', () => {
|
|
196
|
+
it('returns the provider label declared under the aiModelProvider typeDetails', () => {
|
|
197
|
+
const service = createServiceWithSchema({ type: 'string', typeDetails: { aiModelProvider: { label: 'Anthropic' } } });
|
|
198
|
+
expect(service.modelProviderLabel('pref')).to.equal('Anthropic');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('returns undefined when no provider label is declared', () => {
|
|
202
|
+
expect(createServiceWithSchema({ type: 'string' }).modelProviderLabel('pref')).to.equal(undefined);
|
|
203
|
+
expect(createServiceWithSchema({ type: 'string', typeDetails: { other: 'x' } }).modelProviderLabel('pref')).to.equal(undefined);
|
|
204
|
+
// The metadata must be an object carrying a string `label`.
|
|
205
|
+
expect(createServiceWithSchema({ type: 'string', typeDetails: { aiModelProvider: 'Anthropic' } }).modelProviderLabel('pref')).to.equal(undefined);
|
|
206
|
+
expect(createServiceWithSchema({ type: 'string', typeDetails: { aiModelProvider: { label: 42 } } }).modelProviderLabel('pref')).to.equal(undefined);
|
|
207
|
+
expect(createServiceWithSchema(undefined).modelProviderLabel('pref')).to.equal(undefined);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('editInSettings', () => {
|
|
212
|
+
it('opens settings.json focused on the preference via the preferences JSON command', () => {
|
|
213
|
+
const executed: { command: string; args: unknown[] }[] = [];
|
|
214
|
+
const service = new AiSettingsRowService();
|
|
215
|
+
(service as unknown as { commandService: CommandService }).commandService = {
|
|
216
|
+
executeCommand: (command: string, ...args: unknown[]) => {
|
|
217
|
+
executed.push({ command, args });
|
|
218
|
+
return Promise.resolve(undefined);
|
|
219
|
+
}
|
|
220
|
+
} as CommandService;
|
|
221
|
+
|
|
222
|
+
service.editInSettings('my.pref');
|
|
223
|
+
|
|
224
|
+
expect(executed).to.deep.equal([{ command: PreferencesCommands.OPEN_PREFERENCES_JSON_TOOLBAR.id, args: ['my.pref'] }]);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe('normalizeMarkdown', () => {
|
|
229
|
+
const normalize = (markdown: string): string =>
|
|
230
|
+
(new AiSettingsRowService() as unknown as { normalizeMarkdown(md: string): string }).normalizeMarkdown(markdown);
|
|
231
|
+
|
|
232
|
+
it('strips the shared indentation of continuation lines so lists render as lists', () => {
|
|
233
|
+
const raw = [
|
|
234
|
+
'Allows custom settings.',
|
|
235
|
+
' Each setting consists of:',
|
|
236
|
+
' - `scope`: when it applies',
|
|
237
|
+
' - `modelId` (optional)',
|
|
238
|
+
' - `requestSettings`: values'
|
|
239
|
+
].join('\n');
|
|
240
|
+
const expected = [
|
|
241
|
+
'Allows custom settings.',
|
|
242
|
+
'Each setting consists of:',
|
|
243
|
+
'- `scope`: when it applies',
|
|
244
|
+
' - `modelId` (optional)',
|
|
245
|
+
'- `requestSettings`: values'
|
|
246
|
+
].join('\n');
|
|
247
|
+
expect(normalize(raw)).to.equal(expected);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('leaves single-line and already-flush descriptions untouched', () => {
|
|
251
|
+
expect(normalize('A short description.')).to.equal('A short description.');
|
|
252
|
+
expect(normalize('First line.\nSecond line.')).to.equal('First line.\nSecond line.');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('ignores blank lines when computing the shared indentation', () => {
|
|
256
|
+
expect(normalize('Intro.\n\n - item')).to.equal('Intro.\n\n- item');
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('set / reset', () => {
|
|
261
|
+
it('writes to the mapped preference scope', () => {
|
|
262
|
+
const { service, preferences } = createService({});
|
|
263
|
+
return service.set('pref', 42, 'workspace').then(() => {
|
|
264
|
+
expect(preferences.sets).to.deep.equal([{ preferenceId: 'pref', value: 42, scope: PreferenceScope.Workspace, resourceUri: undefined }]);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('resets by writing `undefined` to the mapped scope', () => {
|
|
269
|
+
const { service, preferences } = createService({});
|
|
270
|
+
return service.reset('pref', 'user', 'file:///ws').then(() => {
|
|
271
|
+
expect(preferences.sets).to.deep.equal([{ preferenceId: 'pref', value: undefined, scope: PreferenceScope.User, resourceUri: 'file:///ws' }]);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
});
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource 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 { CommandService, Event } from '@theia/core';
|
|
18
|
+
import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
|
|
19
|
+
import { PreferenceScope } from '@theia/core/lib/common';
|
|
20
|
+
import { PreferenceDataProperty, PreferenceSchemaService } from '@theia/core/lib/common/preferences/preference-schema';
|
|
21
|
+
import { MarkdownRenderer } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer';
|
|
22
|
+
import { PreferencesCommands } from '@theia/preferences/lib/browser/util/preference-types';
|
|
23
|
+
import { AI_CONFIGURATION_SETTING_CONTEXT_MENU, AiConfigurationSettingCommandArgs } from '../ai-configuration-setting-commands';
|
|
24
|
+
import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering/markdown-string';
|
|
25
|
+
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
26
|
+
import type { SelectOption } from '@theia/core/lib/browser/widgets/select-component';
|
|
27
|
+
import { AI_CORE_PREFERENCES_TITLE, MODEL_PROVIDER_TYPE_DETAIL, ModelProviderTypeDetail } from '@theia/ai-core/lib/common/ai-core-preferences';
|
|
28
|
+
import { AiConfigurationService } from '@theia/ai-core/lib/common/ai-configuration-service';
|
|
29
|
+
import { AiConfigurationScope } from '../ai-configuration-category';
|
|
30
|
+
|
|
31
|
+
/** Describes which control an `AiSettingsRow` renders for a preference value. */
|
|
32
|
+
export type AiSettingsControl =
|
|
33
|
+
| { readonly type: 'boolean' }
|
|
34
|
+
| { readonly type: 'string'; readonly placeholder?: string; readonly password?: boolean }
|
|
35
|
+
/** `integer` distinguishes a whole-number preference from a fractional one, which must not be rounded. */
|
|
36
|
+
| { readonly type: 'number'; readonly integer?: boolean; readonly min?: number; readonly max?: number; readonly step?: number }
|
|
37
|
+
| { readonly type: 'select'; readonly options: SelectOption[] }
|
|
38
|
+
/**
|
|
39
|
+
* `validate` refuses an entry before it is written, returning the message to show; used where the values
|
|
40
|
+
* are more than free text (shell command patterns, for instance, have wildcard rules).
|
|
41
|
+
*/
|
|
42
|
+
| { readonly type: 'array'; readonly placeholder?: string; readonly validate?: (value: string) => string | undefined }
|
|
43
|
+
| { readonly type: 'json' };
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The value of a single preference as seen from a particular {@link AiConfigurationScope},
|
|
47
|
+
* together with the information the {@link AiSettingsRow} needs to render the
|
|
48
|
+
* modified indicator and the reset affordance.
|
|
49
|
+
*/
|
|
50
|
+
export interface AiSettingsInspection {
|
|
51
|
+
/** Effective value resolved for the requested scope (falling back through broader scopes and the default). */
|
|
52
|
+
readonly value: unknown;
|
|
53
|
+
/** Value explicitly set in the requested scope; `undefined` means the value is inherited or default. */
|
|
54
|
+
readonly scopeValue: unknown;
|
|
55
|
+
/** Default (schema) value. */
|
|
56
|
+
readonly defaultValue: unknown;
|
|
57
|
+
/** `true` when the requested scope explicitly overrides the value, i.e. a reset is meaningful. */
|
|
58
|
+
readonly modified: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Thin, injectable wrapper around {@link AiConfigurationService} and {@link MarkdownRenderer}
|
|
63
|
+
* used by the presentational {@link AiSettingsRow} component, which must not itself
|
|
64
|
+
* depend on the DI container. Owning renderers/widgets inject this service and pass
|
|
65
|
+
* it down to the row.
|
|
66
|
+
*
|
|
67
|
+
* All preference reads and writes are routed through {@link AiConfigurationService} — the AI
|
|
68
|
+
* configuration seam — rather than the raw `PreferenceService`, so reads are workspace-trust-aware
|
|
69
|
+
* and the whole view stays insulated from how AI preferences are stored (see
|
|
70
|
+
* {@link AiConfigurationService}). Schema introspection still uses {@link PreferenceSchemaService}
|
|
71
|
+
* directly, as it inspects the registered schema shape rather than stored values.
|
|
72
|
+
*/
|
|
73
|
+
@injectable()
|
|
74
|
+
export class AiSettingsRowService {
|
|
75
|
+
|
|
76
|
+
@inject(AiConfigurationService)
|
|
77
|
+
protected readonly aiConfigurationService: AiConfigurationService;
|
|
78
|
+
|
|
79
|
+
@inject(MarkdownRenderer)
|
|
80
|
+
protected readonly markdownRenderer: MarkdownRenderer;
|
|
81
|
+
|
|
82
|
+
@inject(PreferenceSchemaService)
|
|
83
|
+
protected readonly schemaService: PreferenceSchemaService;
|
|
84
|
+
|
|
85
|
+
@inject(CommandService)
|
|
86
|
+
protected readonly commandService: CommandService;
|
|
87
|
+
|
|
88
|
+
@inject(ContextMenuRenderer)
|
|
89
|
+
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
|
90
|
+
|
|
91
|
+
protected changed: Event<void> | undefined;
|
|
92
|
+
|
|
93
|
+
/** Fires whenever an AI preference changes (or the workspace-trust state transitions), so owning widgets can re-render their rows. */
|
|
94
|
+
get onPreferenceChanged(): Event<void> {
|
|
95
|
+
return this.changed ??= Event.map(this.aiConfigurationService.onDidChange, () => undefined);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
inspect(preferenceId: string, scope: AiConfigurationScope, resourceUri?: string): AiSettingsInspection {
|
|
99
|
+
const inspection = this.aiConfigurationService.inspect(preferenceId, resourceUri);
|
|
100
|
+
const defaultValue = inspection?.defaultValue;
|
|
101
|
+
const globalValue = inspection?.globalValue;
|
|
102
|
+
const workspaceValue = inspection?.workspaceValue;
|
|
103
|
+
const folderValue = inspection?.workspaceFolderValue;
|
|
104
|
+
|
|
105
|
+
let scopeValue: unknown;
|
|
106
|
+
let value: unknown;
|
|
107
|
+
switch (scope) {
|
|
108
|
+
case 'user':
|
|
109
|
+
scopeValue = globalValue;
|
|
110
|
+
value = globalValue ?? defaultValue;
|
|
111
|
+
break;
|
|
112
|
+
case 'workspace':
|
|
113
|
+
scopeValue = workspaceValue;
|
|
114
|
+
value = workspaceValue ?? globalValue ?? defaultValue;
|
|
115
|
+
break;
|
|
116
|
+
case 'folder':
|
|
117
|
+
scopeValue = folderValue;
|
|
118
|
+
value = folderValue ?? workspaceValue ?? globalValue ?? defaultValue;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
return { value, scopeValue, defaultValue, modified: scopeValue !== undefined };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
set(preferenceId: string, value: unknown, scope: AiConfigurationScope, resourceUri?: string): Promise<void> {
|
|
125
|
+
return this.aiConfigurationService.set(preferenceId, value, this.toPreferenceScope(scope), resourceUri);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
reset(preferenceId: string, scope: AiConfigurationScope, resourceUri?: string): Promise<void> {
|
|
129
|
+
return this.aiConfigurationService.set(preferenceId, undefined, this.toPreferenceScope(scope), resourceUri);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Opens the underlying `settings.json` file focused on a preference, mirroring the
|
|
134
|
+
* Settings view's "Edit in settings.json" link. Used for `json` controls, i.e. complex
|
|
135
|
+
* object/array values that cannot be edited meaningfully through an inline control.
|
|
136
|
+
*/
|
|
137
|
+
editInSettings(preferenceId: string): void {
|
|
138
|
+
this.commandService.executeCommand(PreferencesCommands.OPEN_PREFERENCES_JSON_TOOLBAR.id, preferenceId);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Opens the gear context menu (Reset Setting / Copy Setting ID / Copy Setting as JSON) anchored below the
|
|
143
|
+
* gear. Uses this view's own {@link AI_CONFIGURATION_SETTING_CONTEXT_MENU} commands, backed by
|
|
144
|
+
* {@link AiConfigurationService} and acting in the row's own `scope` — rather than the Settings UI's
|
|
145
|
+
* `RESET_PREFERENCE` command, which is bound to the Preferences editor widget and misbehaves when invoked
|
|
146
|
+
* from here (it resets in the Preferences widget's scope and disturbs focus in this view).
|
|
147
|
+
*/
|
|
148
|
+
openSettingContextMenu(gear: HTMLElement, preferenceId: string, value: unknown, scope: AiConfigurationScope, resourceUri?: string): void {
|
|
149
|
+
this.openGearMenu(gear, { id: preferenceId, value, scope: this.toPreferenceScope(scope), resourceUri });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Opens the gear menu for a row backed by non-preference data (e.g. a per-agent setting): the same
|
|
154
|
+
* "Reset Setting" affordance as a preference row, but reset runs the given callback. Copy entries are
|
|
155
|
+
* hidden (no preference id). Lets these rows reuse the shared gear menu instead of a bespoke button.
|
|
156
|
+
*/
|
|
157
|
+
openResetMenu(gear: HTMLElement, reset: () => void): void {
|
|
158
|
+
this.openGearMenu(gear, { reset });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
protected openGearMenu(gear: HTMLElement, args: AiConfigurationSettingCommandArgs): void {
|
|
162
|
+
const rect = gear.getBoundingClientRect();
|
|
163
|
+
this.contextMenuRenderer.render({
|
|
164
|
+
menuPath: AI_CONFIGURATION_SETTING_CONTEXT_MENU,
|
|
165
|
+
anchor: { x: rect.left, y: rect.bottom },
|
|
166
|
+
args: [args],
|
|
167
|
+
includeAnchorArg: false,
|
|
168
|
+
context: gear
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Renders a markdown description into a detached element for use in a React `ref`. */
|
|
173
|
+
renderMarkdown(markdown: string): HTMLElement {
|
|
174
|
+
return this.markdownRenderer.render(new MarkdownStringImpl(this.normalizeMarkdown(markdown))).element;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Strips the common leading indentation from a markdown description before rendering.
|
|
179
|
+
*
|
|
180
|
+
* Preference `markdownDescription`s are commonly authored as indented, multi-line JavaScript
|
|
181
|
+
* string literals, which leaves every continuation line prefixed with the source indentation.
|
|
182
|
+
* That accidental indentation makes markdown-it collapse bullet lists and paragraphs into a
|
|
183
|
+
* single dense block, so the description reads as an unstructured run-on. Removing the shared
|
|
184
|
+
* indentation (ignoring the first line, which starts right after the opening quote) restores
|
|
185
|
+
* the intended paragraph and list structure while preserving relative indentation, so nested
|
|
186
|
+
* lists stay nested.
|
|
187
|
+
*/
|
|
188
|
+
protected normalizeMarkdown(markdown: string): string {
|
|
189
|
+
const lines = markdown.split('\n');
|
|
190
|
+
let common = Number.POSITIVE_INFINITY;
|
|
191
|
+
for (let i = 1; i < lines.length; i++) {
|
|
192
|
+
const line = lines[i];
|
|
193
|
+
if (!line.trim()) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
common = Math.min(common, line.length - line.trimStart().length);
|
|
197
|
+
}
|
|
198
|
+
if (!Number.isFinite(common) || common === 0) {
|
|
199
|
+
return markdown;
|
|
200
|
+
}
|
|
201
|
+
return lines.map((line, index) => (index === 0 ? line : line.slice(common))).join('\n');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Reads the label and (already-localized) markdown description a preference declares in its registered
|
|
206
|
+
* schema, so settings rows do not have to re-author those strings. Returns `undefined` fields when the
|
|
207
|
+
* preference is unknown.
|
|
208
|
+
*
|
|
209
|
+
* Most AI preferences share the group `title` ("AI Features") to be grouped together in the Settings UI,
|
|
210
|
+
* which also ignores it and derives each setting's name from its id. We mirror that: a genuinely specific
|
|
211
|
+
* `title` is used as-is, but the shared group title falls back to the humanized preference id (e.g.
|
|
212
|
+
* `ai-features.google.apiKey` → "Api Key") so rows show their own name rather than "AI Features".
|
|
213
|
+
*/
|
|
214
|
+
describe(preferenceId: string): { label?: string; description?: string } {
|
|
215
|
+
const property = this.schemaService.getSchemaProperty(preferenceId);
|
|
216
|
+
if (!property) {
|
|
217
|
+
return { label: undefined, description: undefined };
|
|
218
|
+
}
|
|
219
|
+
const specificTitle = property.title && property.title !== AI_CORE_PREFERENCES_TITLE ? property.title : undefined;
|
|
220
|
+
return {
|
|
221
|
+
label: specificTitle ?? this.formatPreferenceName(preferenceId),
|
|
222
|
+
description: property.markdownDescription ?? property.description
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** The schema tags declared for a preference (e.g. `['experimental']`), shown as badges next to the title. */
|
|
227
|
+
tags(preferenceId: string): string[] {
|
|
228
|
+
return this.schemaService.getSchemaProperty(preferenceId)?.tags ?? [];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Human-readable name for a preference derived from its id. The `ai-features` root and the area segment
|
|
233
|
+
* (`chat`, `google`, …) are dropped — the category/section already conveys them — and the remaining
|
|
234
|
+
* segments are humanized (camelCase split, first letter upper-cased) and joined with ": ", e.g.
|
|
235
|
+
* `ai-features.chat.tokenUsageIndicator.enabled` → "Token Usage Indicator: Enabled",
|
|
236
|
+
* `ai-features.google.apiKey` → "Api Key".
|
|
237
|
+
*/
|
|
238
|
+
protected formatPreferenceName(preferenceId: string): string {
|
|
239
|
+
const parts = preferenceId.split('.');
|
|
240
|
+
const segments = parts.length > 2 ? parts.slice(2) : parts.slice(-1);
|
|
241
|
+
return segments.map(segment => this.humanizeSegment(segment)).join(': ');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
protected humanizeSegment(segment: string): string {
|
|
245
|
+
if (!segment) {
|
|
246
|
+
return segment;
|
|
247
|
+
}
|
|
248
|
+
let formatted = segment[0].toLocaleUpperCase();
|
|
249
|
+
for (let i = 1; i < segment.length; i++) {
|
|
250
|
+
const char = segment[i];
|
|
251
|
+
const previous = segment[i - 1];
|
|
252
|
+
if (this.isUpperCase(char) && !/\s/.test(previous) && !this.isUpperCase(previous)) {
|
|
253
|
+
formatted += ' ';
|
|
254
|
+
}
|
|
255
|
+
formatted += char;
|
|
256
|
+
}
|
|
257
|
+
return formatted.trim();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
protected isUpperCase(char: string): boolean {
|
|
261
|
+
return char === char.toLocaleUpperCase() && char.toLocaleLowerCase() !== char.toLocaleUpperCase();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* The human-readable language-model provider name a preference declares in its schema
|
|
266
|
+
* `typeDetails` (as `{ [MODEL_PROVIDER_TYPE_DETAIL]: { label } }`, see {@link ModelProviderTypeDetail}),
|
|
267
|
+
* or `undefined` when none is declared. The Models page uses this to label a provider block without
|
|
268
|
+
* hard-coding provider names.
|
|
269
|
+
*/
|
|
270
|
+
modelProviderLabel(preferenceId: string): string | undefined {
|
|
271
|
+
const typeDetails = this.schemaService.getSchemaProperty(preferenceId)?.typeDetails;
|
|
272
|
+
const detail = typeDetails && typeof typeDetails === 'object'
|
|
273
|
+
? (typeDetails as Record<string, unknown>)[MODEL_PROVIDER_TYPE_DETAIL]
|
|
274
|
+
: undefined;
|
|
275
|
+
if (detail && typeof detail === 'object') {
|
|
276
|
+
const label = (detail as Partial<ModelProviderTypeDetail>).label;
|
|
277
|
+
if (typeof label === 'string') {
|
|
278
|
+
return label;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Whether a preference declares itself part of a language-model provider block, i.e. carries the
|
|
286
|
+
* `aiModelProvider` {@link MODEL_PROVIDER_TYPE_DETAIL} typeDetail (in any form). The Models page uses
|
|
287
|
+
* this positive marker to discover provider blocks, so feature areas that never claim to be a provider
|
|
288
|
+
* are never mistaken for one.
|
|
289
|
+
*/
|
|
290
|
+
isModelProviderPreference(preferenceId: string): boolean {
|
|
291
|
+
const typeDetails = this.schemaService.getSchemaProperty(preferenceId)?.typeDetails;
|
|
292
|
+
return !!typeDetails && typeof typeDetails === 'object' && MODEL_PROVIDER_TYPE_DETAIL in typeDetails;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Builds {@link SelectOption}s from a preference's `enum`, using its `enumItemLabels`
|
|
297
|
+
* (falling back to `enumDescriptions`, then the raw value) for the display label.
|
|
298
|
+
* Returns an empty array when the preference declares no enum.
|
|
299
|
+
*/
|
|
300
|
+
enumOptions(preferenceId: string): SelectOption[] {
|
|
301
|
+
const property = this.schemaService.getSchemaProperty(preferenceId);
|
|
302
|
+
const values = property?.enum;
|
|
303
|
+
if (!values) {
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
// The label must be the value (or an explicit `enumItemLabels` entry) — never `enumDescriptions`,
|
|
307
|
+
// which is the option's explanatory text and belongs in `description`, not in the label.
|
|
308
|
+
const labels = property?.enumItemLabels;
|
|
309
|
+
const descriptions = property?.enumDescriptions;
|
|
310
|
+
return values.map((value, index) => ({
|
|
311
|
+
value: String(value),
|
|
312
|
+
label: labels?.[index] ?? String(value),
|
|
313
|
+
description: descriptions?.[index]
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** All registered preference ids, e.g. to discover a provider's `ai-features.<provider>.*` block. */
|
|
318
|
+
preferenceIds(): string[] {
|
|
319
|
+
return Array.from(this.schemaService.getSchemaProperties().keys());
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** All displayable `ai-features.*` preference ids — every AI setting that should appear somewhere in the view. */
|
|
323
|
+
aiFeaturePreferenceIds(): string[] {
|
|
324
|
+
return this.preferenceIds().filter(id => id.startsWith('ai-features.') && this.isDisplayable(id));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Whether a preference is meant to be surfaced as an editable settings row in the AI Configuration
|
|
329
|
+
* view. Excludes only value-less placeholders (`type: 'null'`), such as the redirect entries that
|
|
330
|
+
* only link to this view. The Settings-UI `hidden` flag is intentionally ignored: the AI
|
|
331
|
+
* Configuration view is the dedicated editor for AI preferences, which are hidden from the Settings
|
|
332
|
+
* UI once the cutover (#316) is in effect but must still be editable here.
|
|
333
|
+
*/
|
|
334
|
+
isDisplayable(preferenceId: string): boolean {
|
|
335
|
+
const property = this.schemaService.getSchemaProperty(preferenceId);
|
|
336
|
+
if (!property) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
return property.type !== 'null';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Infers a sensible {@link AiSettingsControl} for a preference from its schema `type`
|
|
344
|
+
* (a `select` when it declares an `enum`, a masked text input for secrets/API keys). An `array` of strings
|
|
345
|
+
* gets the inline list editor; more complex values — an `object` or an `array` of objects/other types —
|
|
346
|
+
* cannot be edited meaningfully inline, so they resolve to a `json` control that defers to the
|
|
347
|
+
* `settings.json` file. Falls back to a text input for unknown types.
|
|
348
|
+
*/
|
|
349
|
+
controlFor(preferenceId: string): AiSettingsControl {
|
|
350
|
+
const property = this.schemaService.getSchemaProperty(preferenceId);
|
|
351
|
+
if (property?.enum) {
|
|
352
|
+
return { type: 'select', options: this.enumOptions(preferenceId) };
|
|
353
|
+
}
|
|
354
|
+
switch (property?.type) {
|
|
355
|
+
case 'boolean':
|
|
356
|
+
return { type: 'boolean' };
|
|
357
|
+
case 'number':
|
|
358
|
+
return { type: 'number', min: property.minimum, max: property.maximum };
|
|
359
|
+
case 'integer':
|
|
360
|
+
return { type: 'number', integer: true, min: property.minimum, max: property.maximum };
|
|
361
|
+
case 'array':
|
|
362
|
+
return this.isStringArray(property) ? { type: 'array' } : { type: 'json' };
|
|
363
|
+
case 'object':
|
|
364
|
+
return { type: 'json' };
|
|
365
|
+
default:
|
|
366
|
+
return { type: 'string', password: this.isSecret(preferenceId) };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Whether an `array` preference holds plain string items, so it can be edited with the inline list editor
|
|
372
|
+
* (matching the Settings UI, which only offers its array input for `string[]`); everything else defers to
|
|
373
|
+
* a `json` control.
|
|
374
|
+
*/
|
|
375
|
+
protected isStringArray(property: PreferenceDataProperty): boolean {
|
|
376
|
+
const items = property.items;
|
|
377
|
+
const itemSchema = Array.isArray(items) ? items[0] : items;
|
|
378
|
+
return itemSchema?.type === 'string';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Whether a preference holds a secret (an API key, token or password) that should be masked. There is no
|
|
383
|
+
* schema flag for this, so it is inferred from the id's leaf segment, matching how every AI provider names
|
|
384
|
+
* its key preference (e.g. `ai-features.anthropic.apiKey`).
|
|
385
|
+
*/
|
|
386
|
+
protected isSecret(preferenceId: string): boolean {
|
|
387
|
+
const leaf = preferenceId.substring(preferenceId.lastIndexOf('.') + 1).toLowerCase();
|
|
388
|
+
return leaf.endsWith('apikey') || leaf.endsWith('token') || leaf.endsWith('secret') || leaf.endsWith('password');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
protected toPreferenceScope(scope: AiConfigurationScope): PreferenceScope {
|
|
392
|
+
switch (scope) {
|
|
393
|
+
case 'user':
|
|
394
|
+
return PreferenceScope.User;
|
|
395
|
+
case 'workspace':
|
|
396
|
+
return PreferenceScope.Workspace;
|
|
397
|
+
case 'folder':
|
|
398
|
+
return PreferenceScope.Folder;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|