@theia/ai-ide 1.76.0-next.28 → 1.76.0-next.34
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-tree-widget.d.ts +6 -0
- package/lib/browser/ai-configuration/ai-configuration-tree-widget.d.ts.map +1 -1
- package/lib/browser/ai-configuration/ai-configuration-tree-widget.js +9 -0
- package/lib/browser/ai-configuration/ai-configuration-tree-widget.js.map +1 -1
- package/lib/browser/ai-configuration/categories/agent-capabilities-settings.d.ts.map +1 -1
- package/lib/browser/ai-configuration/categories/agent-capabilities-settings.js +2 -1
- package/lib/browser/ai-configuration/categories/agent-capabilities-settings.js.map +1 -1
- package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.d.ts +8 -0
- package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.d.ts.map +1 -1
- package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.js +27 -8
- package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.js.map +1 -1
- package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.spec.js +38 -2
- package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.spec.js.map +1 -1
- package/lib/browser/ai-configuration/categories/models-configuration-category.d.ts +13 -2
- package/lib/browser/ai-configuration/categories/models-configuration-category.d.ts.map +1 -1
- package/lib/browser/ai-configuration/categories/models-configuration-category.js +41 -2
- package/lib/browser/ai-configuration/categories/models-configuration-category.js.map +1 -1
- package/lib/browser/ai-configuration/categories/models-configuration-category.spec.js +32 -2
- package/lib/browser/ai-configuration/categories/models-configuration-category.spec.js.map +1 -1
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.d.ts +26 -0
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.d.ts.map +1 -0
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.js +212 -0
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.js.map +1 -0
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.d.ts +2 -0
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.d.ts.map +1 -0
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.js +423 -0
- package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.js.map +1 -0
- package/lib/browser/ai-configuration/language-model-renderer.d.ts.map +1 -1
- package/lib/browser/ai-configuration/language-model-renderer.js +10 -3
- package/lib/browser/ai-configuration/language-model-renderer.js.map +1 -1
- package/package.json +25 -25
- package/src/browser/ai-configuration/ai-configuration-tree-widget.tsx +8 -0
- package/src/browser/ai-configuration/categories/agent-capabilities-settings.tsx +7 -8
- package/src/browser/ai-configuration/categories/model-aliases-configuration-category.spec.ts +40 -2
- package/src/browser/ai-configuration/categories/model-aliases-configuration-category.tsx +31 -10
- package/src/browser/ai-configuration/categories/models-configuration-category.spec.ts +47 -2
- package/src/browser/ai-configuration/categories/models-configuration-category.tsx +52 -5
- package/src/browser/ai-configuration/categories/provider-model-discovery-section.spec.tsx +451 -0
- package/src/browser/ai-configuration/categories/provider-model-discovery-section.tsx +341 -0
- package/src/browser/ai-configuration/language-model-renderer.tsx +11 -3
|
@@ -207,11 +207,19 @@ export class AiConfigurationTreeWidget extends TreeWidget {
|
|
|
207
207
|
return match?.target;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Selects a node on behalf of something other than a click on it — a deep link, a restored
|
|
212
|
+
* selection — and reveals what it holds: the category of an item, or the items of a category. A
|
|
213
|
+
* click needs neither, since the node it selects is already visible, and folding a category away
|
|
214
|
+
* only to have the next click on it unfold it again would be its own kind of rude.
|
|
215
|
+
*/
|
|
210
216
|
protected applyTreeSelection(node: SelectableTreeNode): void {
|
|
211
217
|
this.updatingSelection = true;
|
|
212
218
|
try {
|
|
213
219
|
if (AiConfigurationItemNode.is(node) && ExpandableTreeNode.is(node.parent) && !node.parent.expanded) {
|
|
214
220
|
this.model.expandNode(node.parent);
|
|
221
|
+
} else if (AiConfigurationCategoryNode.is(node) && ExpandableTreeNode.is(node) && !node.expanded) {
|
|
222
|
+
this.model.expandNode(node);
|
|
215
223
|
}
|
|
216
224
|
this.model.selectNode(node);
|
|
217
225
|
} finally {
|
|
@@ -25,6 +25,7 @@ import { nls } from '@theia/core';
|
|
|
25
25
|
import { codicon } from '@theia/core/lib/browser';
|
|
26
26
|
import * as React from '@theia/core/shared/react';
|
|
27
27
|
import { AiConfigurationItemRow } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-configuration-item-row';
|
|
28
|
+
import { AiConfigurationIconButton } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-configuration-primitives';
|
|
28
29
|
import { AiSettingsRowService } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-settings-row-service';
|
|
29
30
|
import { AiToggleSwitch } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-configuration-controls';
|
|
30
31
|
|
|
@@ -44,15 +45,13 @@ const AgentSectionHeader: React.FC<{ title: string; action?: React.ReactNode }>
|
|
|
44
45
|
* button, so it reads as a Theia reset affordance. Its tooltip says what the section-wide reset does.
|
|
45
46
|
*/
|
|
46
47
|
const ResetAllButton: React.FC<{ title: string; disabled?: boolean; onReset: () => void }> = ({ title, disabled, onReset }) => (
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
onClick={onReset}
|
|
50
|
-
disabled={disabled}
|
|
48
|
+
<AiConfigurationIconButton
|
|
49
|
+
iconClass={codicon('discard')}
|
|
51
50
|
title={title}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
disabled={disabled}
|
|
52
|
+
className='ai-agent-section-header-action'
|
|
53
|
+
onClick={onReset}
|
|
54
|
+
/>
|
|
56
55
|
);
|
|
57
56
|
|
|
58
57
|
export interface AgentCapabilitiesSettingsProps {
|
package/src/browser/ai-configuration/categories/model-aliases-configuration-category.spec.ts
CHANGED
|
@@ -32,8 +32,8 @@ function alias(id: string, description = ''): LanguageModelAlias {
|
|
|
32
32
|
return { id, description, defaultModelIds: [] } as unknown as LanguageModelAlias;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function readyModel(id: string): LanguageModel {
|
|
36
|
-
return { id, status: { status: 'ready' } } as unknown as LanguageModel;
|
|
35
|
+
function readyModel(id: string, released?: number): LanguageModel {
|
|
36
|
+
return { id, released, status: { status: 'ready' } } as unknown as LanguageModel;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function unreadyModel(id: string): LanguageModel {
|
|
@@ -77,6 +77,44 @@ describe('ModelAliasesConfigurationCategory', () => {
|
|
|
77
77
|
expect(children[2].status?.kind).to.equal('error');
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
+
it('divides the models per provider, newest first, after the default-list entry', () => {
|
|
81
|
+
const category = createCategory([], new Map());
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
83
|
+
(category as any).languageModels = [
|
|
84
|
+
readyModel('openai/gpt-5.5', Date.parse('2025-08-01')),
|
|
85
|
+
readyModel('anthropic/claude-haiku-4-5', Date.parse('2025-10-01')),
|
|
86
|
+
readyModel('openai/gpt-5.6-sol', Date.parse('2026-02-01')),
|
|
87
|
+
readyModel('anthropic/claude-opus-5', Date.parse('2026-04-01'))
|
|
88
|
+
];
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
90
|
+
const options = (category as any).getModelOptions() as Array<{ value?: string; label?: string; separator?: boolean }>;
|
|
91
|
+
expect(options[0].value).to.equal('');
|
|
92
|
+
// A rule between the providers rather than a heading: the select draws separators as plain lines.
|
|
93
|
+
expect(options.slice(1).map(option => option.separator ? '---' : option.value)).to.deep.equal([
|
|
94
|
+
'---',
|
|
95
|
+
'anthropic/claude-opus-5',
|
|
96
|
+
'anthropic/claude-haiku-4-5',
|
|
97
|
+
'---',
|
|
98
|
+
'openai/gpt-5.6-sol',
|
|
99
|
+
'openai/gpt-5.5'
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('keeps the provider rules when mapping the options onto the select', () => {
|
|
104
|
+
const category = createCategory([], new Map());
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
106
|
+
const mapped = (category as any).toEnumOptions([
|
|
107
|
+
{ value: 'a', label: 'A', description: 'first' },
|
|
108
|
+
{ separator: true },
|
|
109
|
+
{ value: 'b' }
|
|
110
|
+
]) as Array<{ value: string; label: string; title?: string; separator?: boolean }>;
|
|
111
|
+
// A rule that loses its `separator` becomes a selectable entry with no label that clears the alias.
|
|
112
|
+
expect(mapped.map(option => option.separator ? '---' : option.value)).to.deep.equal(['a', '---', 'b']);
|
|
113
|
+
expect(mapped[0].title).to.equal('first');
|
|
114
|
+
// Without a label of its own, an option falls back to its value rather than rendering empty.
|
|
115
|
+
expect(mapped[2].label).to.equal('b');
|
|
116
|
+
});
|
|
117
|
+
|
|
80
118
|
it('indexes one search item per alias, navigating to the item', () => {
|
|
81
119
|
const category = createCategory([alias('default/code', 'Coding alias')], new Map());
|
|
82
120
|
const items = category.getSearchItems();
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
|
-
import { AgentService, AISettingsService, LANGUAGE_MODEL_ALIASES_PREFERENCE } from '@theia/ai-core';
|
|
17
|
+
import { AgentService, AISettingsService, groupModelsByProvider, LANGUAGE_MODEL_ALIASES_PREFERENCE } from '@theia/ai-core';
|
|
18
18
|
import { FrontendLanguageModelRegistry, LanguageModel, LanguageModelRegistry, LanguageModelRequirement } from '@theia/ai-core/lib/common/language-model';
|
|
19
19
|
import { LanguageModelAlias, LanguageModelAliasRegistry } from '@theia/ai-core/lib/common/language-model-alias';
|
|
20
20
|
import { Emitter, Event, nls } from '@theia/core';
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
} from '@theia/ai-core-ui/lib/browser/ai-configuration/ai-configuration-category';
|
|
36
36
|
import { CollectionCategoryRenderer } from '@theia/ai-core-ui/lib/browser/ai-configuration/renderers/collection-category-renderer';
|
|
37
37
|
import { AiConfigurationSection } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-configuration-primitives';
|
|
38
|
-
import { AiEnumSelect } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-configuration-controls';
|
|
38
|
+
import { AiEnumOption, AiEnumSelect } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-configuration-controls';
|
|
39
39
|
import { AgentChips } from './agent-chips';
|
|
40
40
|
|
|
41
41
|
/** Sentinel option value representing "use the alias' default priority list" (no explicit model). */
|
|
@@ -214,28 +214,49 @@ export class ModelAliasesConfigurationCategory extends CollectionCategoryRendere
|
|
|
214
214
|
ariaLabel={nls.localize('theia/ai/core/modelAliasesConfiguration/selectedModelId', 'Selected Model')}
|
|
215
215
|
value={isInvalid ? undefined : selected}
|
|
216
216
|
invalid={isInvalid}
|
|
217
|
-
options={
|
|
217
|
+
options={this.toEnumOptions(options)}
|
|
218
218
|
onCommit={value => this.setSelectedModel(alias, value)}
|
|
219
219
|
/>
|
|
220
220
|
</AiConfigurationSection>;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Maps the options onto what {@link AiEnumSelect} takes. The per-provider rules have to survive the
|
|
225
|
+
* mapping: dropping `separator` turns each of them into a selectable entry with no label that
|
|
226
|
+
* clears the alias when picked.
|
|
227
|
+
*/
|
|
228
|
+
protected toEnumOptions(options: SelectOption[]): AiEnumOption[] {
|
|
229
|
+
return options.map(option => ({
|
|
230
|
+
value: String(option.value ?? ''),
|
|
231
|
+
label: option.label ?? String(option.value ?? ''),
|
|
232
|
+
title: option.description,
|
|
233
|
+
separator: option.separator
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
|
|
223
237
|
protected getModelOptions(): SelectOption[] {
|
|
224
238
|
const options: SelectOption[] = [{
|
|
225
239
|
value: DEFAULT_LIST_VALUE,
|
|
226
240
|
label: nls.localize('theia/ai/core/modelAliasesConfiguration/defaultList', '[Default list]')
|
|
227
241
|
}];
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
description: !ready ? model.status.message : undefined
|
|
234
|
-
});
|
|
242
|
+
// Divided per provider, newest first inside each group. An alias names a concrete model, which
|
|
243
|
+
// includes the older and release-pinned ones, so the whole catalogue is offered here.
|
|
244
|
+
for (const { models } of groupModelsByProvider(this.languageModels)) {
|
|
245
|
+
options.push({ separator: true });
|
|
246
|
+
options.push(...models.map(model => this.toModelOption(model)));
|
|
235
247
|
}
|
|
236
248
|
return options;
|
|
237
249
|
}
|
|
238
250
|
|
|
251
|
+
protected toModelOption(model: LanguageModel): SelectOption {
|
|
252
|
+
const ready = model.status.status === 'ready';
|
|
253
|
+
return {
|
|
254
|
+
value: model.id,
|
|
255
|
+
label: `${model.name ?? model.id} ${ready ? '✓' : '✗'}`,
|
|
256
|
+
description: !ready ? model.status.message : undefined
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
239
260
|
protected setSelectedModel(alias: LanguageModelAlias, value: string | undefined): void {
|
|
240
261
|
const selectedModelId = value ? value : undefined;
|
|
241
262
|
this.languageModelAliasRegistry.ready.then(() => this.languageModelAliasRegistry.addAlias({ ...alias, selectedModelId }));
|
|
@@ -23,6 +23,8 @@ FrontendApplicationConfigProvider.set({});
|
|
|
23
23
|
import { expect } from 'chai';
|
|
24
24
|
import { AiSettingsControl, AiSettingsRowService } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-settings-row-service';
|
|
25
25
|
import { AiConfigurationCategoryId } from '@theia/ai-core-ui/lib/browser/ai-configuration/ai-configuration-category';
|
|
26
|
+
import { ModelDiscoveryStatusService } from '@theia/ai-core/lib/browser';
|
|
27
|
+
import { ModelDiscoveryStatus } from '@theia/ai-core/lib/common/model-discovery-status';
|
|
26
28
|
import { ModelsConfigurationCategory } from './models-configuration-category';
|
|
27
29
|
|
|
28
30
|
disableJSDOM();
|
|
@@ -69,7 +71,7 @@ describe('ModelsConfigurationCategory', () => {
|
|
|
69
71
|
'ai-features.modelSettings.requestSettings',
|
|
70
72
|
'ai-features.reasoning.defaults',
|
|
71
73
|
'ai-features.anthropic.AnthropicApiKey',
|
|
72
|
-
'ai-features.anthropic.
|
|
74
|
+
'ai-features.anthropic.serverSideCompaction',
|
|
73
75
|
'ai-features.google.apiKey',
|
|
74
76
|
// must be ignored: owned by other categories / unrelated
|
|
75
77
|
'ai-features.chat.pinChatAgent',
|
|
@@ -84,7 +86,7 @@ describe('ModelsConfigurationCategory', () => {
|
|
|
84
86
|
const sections = (category as any).getSections() as Array<{ id: string; preferenceIds: string[] }>;
|
|
85
87
|
expect(sections.map(s => s.id)).to.deep.equal(['model-settings', 'anthropic', 'google']);
|
|
86
88
|
expect(sections[0].preferenceIds).to.have.members(['ai-features.modelSettings.requestSettings', 'ai-features.reasoning.defaults']);
|
|
87
|
-
expect(sections[1].preferenceIds).to.deep.equal(['ai-features.anthropic.AnthropicApiKey', 'ai-features.anthropic.
|
|
89
|
+
expect(sections[1].preferenceIds).to.deep.equal(['ai-features.anthropic.AnthropicApiKey', 'ai-features.anthropic.serverSideCompaction']);
|
|
88
90
|
expect(sections[2].preferenceIds).to.deep.equal(['ai-features.google.apiKey']);
|
|
89
91
|
});
|
|
90
92
|
|
|
@@ -164,4 +166,47 @@ describe('ModelsConfigurationCategory', () => {
|
|
|
164
166
|
highlight: { rowId: 'ai-features.anthropic.AnthropicApiKey' }
|
|
165
167
|
});
|
|
166
168
|
});
|
|
169
|
+
|
|
170
|
+
describe('model discovery section', () => {
|
|
171
|
+
|
|
172
|
+
function withDiscovery(statuses: ModelDiscoveryStatus[], modelIds: string[]): ModelsConfigurationCategory {
|
|
173
|
+
const category = createCategory([]);
|
|
174
|
+
const service: Partial<ModelDiscoveryStatusService> = {
|
|
175
|
+
getStatus: (providerId: string) => statuses.find(status => status.providerId === providerId)
|
|
176
|
+
};
|
|
177
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
178
|
+
(category as any).discoveryStatus = service;
|
|
179
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
180
|
+
(category as any).models = modelIds.map(id => ({ id }));
|
|
181
|
+
return category;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function discoveredModelIds(category: ModelsConfigurationCategory, providerId: string): string[] | undefined {
|
|
185
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
186
|
+
const rendered = (category as any).renderModelDiscovery(providerId) as
|
|
187
|
+
{ props: { models: { id: string }[] } } | undefined;
|
|
188
|
+
return rendered && rendered.props.models.map(model => model.id);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
it('renders nothing for a provider that does not participate in discovery', () => {
|
|
192
|
+
const category = withDiscovery([], ['anthropic/claude-opus-5']);
|
|
193
|
+
expect(discoveredModelIds(category, 'anthropic')).to.equal(undefined);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('lists the provider\'s own models, keyed by the provider id', () => {
|
|
197
|
+
const category = withDiscovery(
|
|
198
|
+
[{ providerId: 'anthropic', label: 'Anthropic', state: 'ready' }],
|
|
199
|
+
['anthropic/claude-opus-5', 'google/gemini-3.1-pro', 'anthropic-custom/foo']
|
|
200
|
+
);
|
|
201
|
+
expect(discoveredModelIds(category, 'anthropic')).to.deep.equal(['anthropic/claude-opus-5']);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('honours a model-id prefix that differs from the provider id', () => {
|
|
205
|
+
const category = withDiscovery(
|
|
206
|
+
[{ providerId: 'openAiOfficial', label: 'OpenAI', modelIdPrefix: 'openai', state: 'ready' }],
|
|
207
|
+
['openai/gpt-5.5', 'openAiOfficial/gpt-5.5', 'openai-custom/local']
|
|
208
|
+
);
|
|
209
|
+
expect(discoveredModelIds(category, 'openAiOfficial')).to.deep.equal(['openai/gpt-5.5']);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
167
212
|
});
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
|
-
import { Emitter, Event, nls } from '@theia/core';
|
|
17
|
+
import { CommandService, Emitter, Event, nls } from '@theia/core';
|
|
18
18
|
import { codicon } from '@theia/core/lib/browser';
|
|
19
19
|
import { DisposableCollection } from '@theia/core/lib/common';
|
|
20
20
|
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
|
@@ -32,6 +32,9 @@ import { AiSettingsRowService } from '@theia/ai-core-ui/lib/browser/ai-configura
|
|
|
32
32
|
import { AiConfigurationSection } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-configuration-primitives';
|
|
33
33
|
import { CollectionCategoryRenderer } from '@theia/ai-core-ui/lib/browser/ai-configuration/renderers/collection-category-renderer';
|
|
34
34
|
import { AiSettingsRow } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-settings-row';
|
|
35
|
+
import { LanguageModel, LanguageModelRegistry } from '@theia/ai-core/lib/common';
|
|
36
|
+
import { FavoriteModelsService, ModelDiscoveryStatusService } from '@theia/ai-core/lib/browser';
|
|
37
|
+
import { ProviderModelDiscoverySection } from './provider-model-discovery-section';
|
|
35
38
|
|
|
36
39
|
/** Prefix shared by every AI preference. */
|
|
37
40
|
const AI_FEATURES_PREFIX = 'ai-features.';
|
|
@@ -73,10 +76,25 @@ export class ModelsConfigurationCategory extends CollectionCategoryRenderer impl
|
|
|
73
76
|
@inject(AiSettingsRowService)
|
|
74
77
|
protected readonly settingsRowService: AiSettingsRowService;
|
|
75
78
|
|
|
79
|
+
@inject(ModelDiscoveryStatusService)
|
|
80
|
+
protected readonly discoveryStatus: ModelDiscoveryStatusService;
|
|
81
|
+
|
|
82
|
+
@inject(FavoriteModelsService)
|
|
83
|
+
protected readonly favoriteModels: FavoriteModelsService;
|
|
84
|
+
|
|
85
|
+
@inject(CommandService)
|
|
86
|
+
protected readonly commandService: CommandService;
|
|
87
|
+
|
|
88
|
+
@inject(LanguageModelRegistry)
|
|
89
|
+
protected readonly languageModelRegistry: LanguageModelRegistry;
|
|
90
|
+
|
|
76
91
|
protected readonly onDidChangeEmitter = new Emitter<void>();
|
|
77
92
|
readonly onDidChange: Event<void> = this.onDidChangeEmitter.event;
|
|
78
93
|
protected readonly toDispose = new DisposableCollection(this.onDidChangeEmitter);
|
|
79
94
|
|
|
95
|
+
/** Registered models, kept in sync so the provider detail can list them synchronously while rendering. */
|
|
96
|
+
protected models: LanguageModel[] = [];
|
|
97
|
+
|
|
80
98
|
get renderer(): this {
|
|
81
99
|
return this;
|
|
82
100
|
}
|
|
@@ -96,6 +114,14 @@ export class ModelsConfigurationCategory extends CollectionCategoryRenderer impl
|
|
|
96
114
|
@postConstruct()
|
|
97
115
|
protected init(): void {
|
|
98
116
|
this.toDispose.push(this.settingsRowService.onPreferenceChanged(() => this.onDidChangeEmitter.fire()));
|
|
117
|
+
this.toDispose.push(this.discoveryStatus.onDidChange(() => this.onDidChangeEmitter.fire()));
|
|
118
|
+
this.toDispose.push(this.languageModelRegistry.onChange(() => this.updateModels()));
|
|
119
|
+
this.updateModels();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
protected async updateModels(): Promise<void> {
|
|
123
|
+
this.models = await this.languageModelRegistry.getLanguageModels();
|
|
124
|
+
this.onDidChangeEmitter.fire();
|
|
99
125
|
}
|
|
100
126
|
|
|
101
127
|
dispose(): void {
|
|
@@ -130,15 +156,36 @@ export class ModelsConfigurationCategory extends CollectionCategoryRenderer impl
|
|
|
130
156
|
</AiConfigurationSection>;
|
|
131
157
|
}
|
|
132
158
|
|
|
133
|
-
/** Provider detail (node selected): that provider's settings. */
|
|
159
|
+
/** Provider detail (node selected): that provider's settings, followed by its model discovery. */
|
|
134
160
|
protected renderItemSections(item: AiConfigurationTreeItem, ctx: AiConfigurationRenderContext): React.ReactNode {
|
|
135
161
|
const section = this.getProviderSections().find(candidate => candidate.id === item.id);
|
|
136
162
|
if (!section) {
|
|
137
163
|
return undefined;
|
|
138
164
|
}
|
|
139
|
-
return
|
|
140
|
-
{
|
|
141
|
-
|
|
165
|
+
return <>
|
|
166
|
+
<AiConfigurationSection title={nls.localizeByDefault('Settings')}>
|
|
167
|
+
{section.preferenceIds.map(preferenceId => this.renderPreferenceRow(ctx, preferenceId))}
|
|
168
|
+
</AiConfigurationSection>
|
|
169
|
+
{this.renderModelDiscovery(item.id)}
|
|
170
|
+
</>;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Only providers that participate in dynamic model discovery contribute this section. */
|
|
174
|
+
protected renderModelDiscovery(providerId: string): React.ReactNode {
|
|
175
|
+
const status = this.discoveryStatus.getStatus(providerId);
|
|
176
|
+
if (!status) {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
// A provider's registered model ids may carry a prefix that differs from its preference segment
|
|
180
|
+
// (e.g. the `openAiOfficial` block registers `openai/…` models).
|
|
181
|
+
const prefix = `${status.modelIdPrefix ?? providerId}/`;
|
|
182
|
+
return <ProviderModelDiscoverySection
|
|
183
|
+
status={status}
|
|
184
|
+
models={this.models.filter(model => model.id.startsWith(prefix))}
|
|
185
|
+
favorites={this.favoriteModels}
|
|
186
|
+
onRefresh={() => this.discoveryStatus.refresh(providerId)}
|
|
187
|
+
onAction={action => this.commandService.executeCommand(action.commandId, ...(action.args ?? []))}
|
|
188
|
+
/>;
|
|
142
189
|
}
|
|
143
190
|
|
|
144
191
|
protected renderPreferenceRow(ctx: AiConfigurationRenderContext, preferenceId: string): React.ReactNode {
|