@theia/ai-ide 1.76.0-next.7 → 1.76.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-allow-all-mode-chat-banner.js +1 -1
- package/lib/browser/ai-allow-all-mode-chat-banner.js.map +1 -1
- package/lib/browser/ai-aware-scm-commit-widget.d.ts +27 -0
- package/lib/browser/ai-aware-scm-commit-widget.d.ts.map +1 -0
- package/lib/browser/ai-aware-scm-commit-widget.js +142 -0
- package/lib/browser/ai-aware-scm-commit-widget.js.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-detail-widget.d.ts +10 -4
- package/lib/browser/ai-configuration/ai-configuration-detail-widget.d.ts.map +1 -1
- package/lib/browser/ai-configuration/ai-configuration-detail-widget.js +12 -7
- package/lib/browser/ai-configuration/ai-configuration-detail-widget.js.map +1 -1
- package/lib/browser/ai-configuration/ai-configuration-detail-widget.spec.js +14 -14
- package/lib/browser/ai-configuration/ai-configuration-detail-widget.spec.js.map +1 -1
- 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/agent-chips.js +1 -1
- package/lib/browser/ai-configuration/categories/agent-chips.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/lib/browser/ai-getting-started-commands.d.ts +10 -0
- package/lib/browser/ai-getting-started-commands.d.ts.map +1 -0
- package/lib/browser/ai-getting-started-commands.js +34 -0
- package/lib/browser/ai-getting-started-commands.js.map +1 -0
- package/lib/browser/ai-getting-started-contribution.d.ts +20 -0
- package/lib/browser/ai-getting-started-contribution.d.ts.map +1 -0
- package/lib/browser/ai-getting-started-contribution.js +70 -0
- package/lib/browser/ai-getting-started-contribution.js.map +1 -0
- package/lib/browser/ai-getting-started-walkthrough.d.ts +41 -0
- package/lib/browser/ai-getting-started-walkthrough.d.ts.map +1 -0
- package/lib/browser/ai-getting-started-walkthrough.js +225 -0
- package/lib/browser/ai-getting-started-walkthrough.js.map +1 -0
- package/lib/browser/ai-getting-started-walkthrough.spec.d.ts +2 -0
- package/lib/browser/ai-getting-started-walkthrough.spec.d.ts.map +1 -0
- package/lib/browser/ai-getting-started-walkthrough.spec.js +124 -0
- package/lib/browser/ai-getting-started-walkthrough.spec.js.map +1 -0
- package/lib/browser/ai-walkthrough-context-keys.d.ts +55 -0
- package/lib/browser/ai-walkthrough-context-keys.d.ts.map +1 -0
- package/lib/browser/ai-walkthrough-context-keys.js +145 -0
- package/lib/browser/ai-walkthrough-context-keys.js.map +1 -0
- package/lib/browser/ai-walkthrough-context-keys.spec.d.ts +2 -0
- package/lib/browser/ai-walkthrough-context-keys.spec.d.ts.map +1 -0
- package/lib/browser/ai-walkthrough-context-keys.spec.js +166 -0
- package/lib/browser/ai-walkthrough-context-keys.spec.js.map +1 -0
- package/lib/browser/chat-session-item.js +2 -2
- package/lib/browser/chat-session-item.js.map +1 -1
- package/lib/browser/commit-message-agent.d.ts +31 -0
- package/lib/browser/commit-message-agent.d.ts.map +1 -0
- package/lib/browser/commit-message-agent.js +99 -0
- package/lib/browser/commit-message-agent.js.map +1 -0
- package/lib/browser/commit-message-agent.spec.d.ts +2 -0
- package/lib/browser/commit-message-agent.spec.d.ts.map +1 -0
- package/lib/browser/commit-message-agent.spec.js +82 -0
- package/lib/browser/commit-message-agent.spec.js.map +1 -0
- package/lib/browser/commit-message-command-contribution.d.ts +15 -0
- package/lib/browser/commit-message-command-contribution.d.ts.map +1 -0
- package/lib/browser/commit-message-command-contribution.js +73 -0
- package/lib/browser/commit-message-command-contribution.js.map +1 -0
- package/lib/browser/commit-message-commands.d.ts +5 -0
- package/lib/browser/commit-message-commands.d.ts.map +1 -0
- package/lib/browser/commit-message-commands.js +28 -0
- package/lib/browser/commit-message-commands.js.map +1 -0
- package/lib/browser/commit-message-prompt-template.d.ts +11 -0
- package/lib/browser/commit-message-prompt-template.d.ts.map +1 -0
- package/lib/browser/commit-message-prompt-template.js +63 -0
- package/lib/browser/commit-message-prompt-template.js.map +1 -0
- package/lib/browser/commit-message-runner.d.ts +41 -0
- package/lib/browser/commit-message-runner.d.ts.map +1 -0
- package/lib/browser/commit-message-runner.js +143 -0
- package/lib/browser/commit-message-runner.js.map +1 -0
- package/lib/browser/commit-message-runner.spec.d.ts +2 -0
- package/lib/browser/commit-message-runner.spec.d.ts.map +1 -0
- package/lib/browser/commit-message-runner.spec.js +206 -0
- package/lib/browser/commit-message-runner.spec.js.map +1 -0
- package/lib/browser/frontend-module.d.ts.map +1 -1
- package/lib/browser/frontend-module.js +25 -0
- package/lib/browser/frontend-module.js.map +1 -1
- package/lib/browser/git-changes-tool.d.ts +73 -0
- package/lib/browser/git-changes-tool.d.ts.map +1 -0
- package/lib/browser/git-changes-tool.js +217 -0
- package/lib/browser/git-changes-tool.js.map +1 -0
- package/lib/browser/git-changes-tool.spec.d.ts +2 -0
- package/lib/browser/git-changes-tool.spec.d.ts.map +1 -0
- package/lib/browser/git-changes-tool.spec.js +167 -0
- package/lib/browser/git-changes-tool.spec.js.map +1 -0
- package/lib/browser/ide-chat-welcome-message-provider.d.ts +26 -28
- package/lib/browser/ide-chat-welcome-message-provider.d.ts.map +1 -1
- package/lib/browser/ide-chat-welcome-message-provider.js +61 -151
- package/lib/browser/ide-chat-welcome-message-provider.js.map +1 -1
- package/lib/browser/review/pr-review-capability-contribution.js +1 -1
- package/lib/browser/review/pr-review-capability-contribution.js.map +1 -1
- package/lib/browser/user-interaction-tool-renderer.d.ts +6 -0
- package/lib/browser/user-interaction-tool-renderer.d.ts.map +1 -1
- package/lib/browser/user-interaction-tool-renderer.js +36 -7
- package/lib/browser/user-interaction-tool-renderer.js.map +1 -1
- package/lib/browser/user-interaction-tool.d.ts +23 -1
- package/lib/browser/user-interaction-tool.d.ts.map +1 -1
- package/lib/browser/user-interaction-tool.js +56 -17
- package/lib/browser/user-interaction-tool.js.map +1 -1
- package/lib/browser/user-interaction-tool.spec.js +151 -2
- package/lib/browser/user-interaction-tool.spec.js.map +1 -1
- package/lib/common/command-prompt-template.js +1 -1
- package/lib/common/user-interaction-tool.d.ts +19 -1
- package/lib/common/user-interaction-tool.d.ts.map +1 -1
- package/lib/common/user-interaction-tool.js +100 -29
- package/lib/common/user-interaction-tool.js.map +1 -1
- package/lib/common/user-interaction-tool.spec.js +215 -174
- package/lib/common/user-interaction-tool.spec.js.map +1 -1
- package/package.json +26 -25
- package/src/browser/ai-allow-all-mode-chat-banner.tsx +2 -2
- package/src/browser/ai-aware-scm-commit-widget.tsx +153 -0
- package/src/browser/ai-configuration/ai-configuration-detail-widget.spec.ts +14 -14
- package/src/browser/ai-configuration/ai-configuration-detail-widget.tsx +12 -7
- 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/agent-chips.tsx +1 -1
- 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
- package/src/browser/ai-getting-started-commands.ts +33 -0
- package/src/browser/ai-getting-started-contribution.ts +64 -0
- package/src/browser/ai-getting-started-walkthrough.spec.ts +146 -0
- package/src/browser/ai-getting-started-walkthrough.ts +249 -0
- package/src/browser/ai-walkthrough-context-keys.spec.ts +209 -0
- package/src/browser/ai-walkthrough-context-keys.ts +147 -0
- package/src/browser/chat-session-item.tsx +2 -2
- package/src/browser/commit-message-agent.spec.ts +97 -0
- package/src/browser/commit-message-agent.ts +113 -0
- package/src/browser/commit-message-command-contribution.ts +67 -0
- package/src/browser/commit-message-commands.ts +25 -0
- package/src/browser/commit-message-prompt-template.ts +65 -0
- package/src/browser/commit-message-runner.spec.ts +266 -0
- package/src/browser/commit-message-runner.ts +163 -0
- package/src/browser/frontend-module.ts +27 -0
- package/src/browser/git-changes-tool.spec.ts +200 -0
- package/src/browser/git-changes-tool.ts +239 -0
- package/src/browser/ide-chat-welcome-message-provider.tsx +93 -230
- package/src/browser/review/pr-review-capability-contribution.ts +1 -1
- package/src/browser/style/commit-message-overlay.css +64 -0
- package/src/browser/style/index.css +4 -42
- package/src/browser/user-interaction-tool-renderer.tsx +38 -8
- package/src/browser/user-interaction-tool.spec.ts +201 -2
- package/src/browser/user-interaction-tool.ts +69 -18
- package/src/common/command-prompt-template.ts +1 -1
- package/src/common/user-interaction-tool.spec.ts +217 -161
- package/src/common/user-interaction-tool.ts +127 -32
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';
|
|
18
|
+
import * as React from '@theia/core/shared/react';
|
|
19
|
+
import { CommandService, ILogger, nls } from '@theia/core';
|
|
20
|
+
import { codicon } from '@theia/core/lib/browser';
|
|
21
|
+
import { AgentService } from '@theia/ai-core/lib/common';
|
|
22
|
+
import { AIActivationService } from '@theia/ai-core/lib/browser';
|
|
23
|
+
import { ScmCommitWidget } from '@theia/scm/lib/browser/scm-commit-widget';
|
|
24
|
+
import { ScmInput } from '@theia/scm/lib/browser/scm-input';
|
|
25
|
+
import { ScmProvider } from '@theia/scm/lib/browser/scm-provider';
|
|
26
|
+
import { COMMIT_MESSAGE_AGENT_ID } from './commit-message-agent';
|
|
27
|
+
import { CommitMessageCommands } from './commit-message-commands';
|
|
28
|
+
import { CommitMessageRunner } from './commit-message-runner';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Source-control provider id of the VS Code / Theia git extension. The generator runs
|
|
32
|
+
* `git diff --cached`, so it is git-specific by construction and the overlay is only offered
|
|
33
|
+
* for that provider rather than for any provider that happens to expose a matching group.
|
|
34
|
+
*/
|
|
35
|
+
const GIT_PROVIDER_ID = 'git';
|
|
36
|
+
|
|
37
|
+
/** Resource group the git extension uses for staged resources. */
|
|
38
|
+
const STAGED_GROUP_ID = 'index';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Extends the standard SCM commit-message widget with an overlay icon that generates a commit
|
|
42
|
+
* message from the staged changes. Bound via a DI rebind of {@link ScmCommitWidget} so the SCM
|
|
43
|
+
* view picks up the AI-aware version transparently.
|
|
44
|
+
*/
|
|
45
|
+
@injectable()
|
|
46
|
+
export class AiAwareScmCommitWidget extends ScmCommitWidget {
|
|
47
|
+
|
|
48
|
+
@inject(CommandService)
|
|
49
|
+
protected readonly commandService: CommandService;
|
|
50
|
+
|
|
51
|
+
@inject(CommitMessageRunner)
|
|
52
|
+
protected readonly commitMessageRunner: CommitMessageRunner;
|
|
53
|
+
|
|
54
|
+
@inject(AgentService)
|
|
55
|
+
protected readonly agentService: AgentService;
|
|
56
|
+
|
|
57
|
+
@inject(AIActivationService)
|
|
58
|
+
protected readonly aiActivationService: AIActivationService;
|
|
59
|
+
|
|
60
|
+
@inject(ILogger) @named('ai-ide:AiAwareScmCommitWidget')
|
|
61
|
+
protected readonly logger: ILogger;
|
|
62
|
+
|
|
63
|
+
@postConstruct()
|
|
64
|
+
protected initAiOverlay(): void {
|
|
65
|
+
this.toDispose.push(this.commitMessageRunner.onDidChange(() => this.update()));
|
|
66
|
+
this.toDispose.push(this.agentService.onDidChangeAgents(() => this.update()));
|
|
67
|
+
this.toDispose.push(this.aiActivationService.onDidChangeActiveStatus(() => this.update()));
|
|
68
|
+
// `canRun` carries conditions beyond `isActive` (e.g. workspace trust) and is what the
|
|
69
|
+
// command's `isEnabled` is gated on, so the button's enabled state has to follow it.
|
|
70
|
+
this.toDispose.push(this.aiActivationService.onDidChangeCanRun(() => this.update()));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
protected override renderInput(input: ScmInput): React.ReactNode {
|
|
74
|
+
const baseInput = super.renderInput(input);
|
|
75
|
+
// The wrapper adds padding to make room for the button, so it must only be rendered when
|
|
76
|
+
// a button is actually rendered into it. Users with AI off keep the stock SCM input.
|
|
77
|
+
if (!this.shouldRenderAiButton(input)) {
|
|
78
|
+
return baseInput;
|
|
79
|
+
}
|
|
80
|
+
return <div className='theia-ai-commit-message-input-wrapper'>
|
|
81
|
+
{baseInput}
|
|
82
|
+
{this.renderAiOverlay(input)}
|
|
83
|
+
</div>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
protected shouldRenderAiButton(input: ScmInput): boolean {
|
|
87
|
+
if (!input.visible || !this.aiActivationService.isActive) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
// Without staged changes `git diff --cached` is empty, so the button would have nothing to
|
|
91
|
+
// work with. A running generation keeps it rendered so it stays cancellable even if the
|
|
92
|
+
// user unstages everything mid-run.
|
|
93
|
+
return this.commitMessageRunner.isRunning() || this.hasStagedChanges(this.scmService.selectedRepository?.provider);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
protected renderAiOverlay(input: ScmInput): React.ReactNode {
|
|
97
|
+
const running = this.commitMessageRunner.isRunning();
|
|
98
|
+
const agentEnabled = this.agentService.isEnabled(COMMIT_MESSAGE_AGENT_ID);
|
|
99
|
+
// Mirrors `CommitMessageCommandContribution.isEnabled` as wrapped by
|
|
100
|
+
// `AICommandHandlerFactory`; a divergence would render an enabled button whose click ends
|
|
101
|
+
// in a `NO_ACTIVE_HANDLER` error. Cancelling is always allowed, so that the spinner keeps
|
|
102
|
+
// working while the git extension has the input disabled during a commit.
|
|
103
|
+
const disabled = !running && (!this.aiActivationService.canRun || !agentEnabled || !input.enabled);
|
|
104
|
+
|
|
105
|
+
let title: string;
|
|
106
|
+
if (running) {
|
|
107
|
+
title = nls.localize('theia/ai-ide/commit-message/cancel-tooltip', 'Cancel commit-message generation');
|
|
108
|
+
} else if (!agentEnabled) {
|
|
109
|
+
title = nls.localize(
|
|
110
|
+
'theia/ai-ide/commit-message/agent-disabled-tooltip',
|
|
111
|
+
'The Commit Message agent is disabled. Enable it in the AI Configuration view.'
|
|
112
|
+
);
|
|
113
|
+
} else {
|
|
114
|
+
title = nls.localize(
|
|
115
|
+
'theia/ai-ide/commit-message/staged-tooltip',
|
|
116
|
+
'Generate commit message from staged changes'
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const iconClass = running ? `${codicon('loading')} codicon-modifier-spin` : codicon('sparkle');
|
|
120
|
+
|
|
121
|
+
return <div className='theia-ai-commit-message-overlay'>
|
|
122
|
+
<button
|
|
123
|
+
className='theia-ai-commit-message-icon'
|
|
124
|
+
type='button'
|
|
125
|
+
title={title}
|
|
126
|
+
aria-label={title}
|
|
127
|
+
disabled={disabled}
|
|
128
|
+
onClick={this.onAiButtonClick}>
|
|
129
|
+
<span className={iconClass} />
|
|
130
|
+
</button>
|
|
131
|
+
</div>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
protected onAiButtonClick = () => {
|
|
135
|
+
// Cancelling never goes through the command: its `isEnabled` requires an enabled commit
|
|
136
|
+
// input and a runnable AI, either of which may have flipped off since the run started.
|
|
137
|
+
if (this.commitMessageRunner.isRunning()) {
|
|
138
|
+
this.commitMessageRunner.cancel();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
this.commandService.executeCommand(CommitMessageCommands.GENERATE_FROM_STAGED.id).catch(error =>
|
|
142
|
+
this.logger.error('Failed to execute AI commit-message command', error)
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
protected hasStagedChanges(provider: ScmProvider | undefined): boolean {
|
|
147
|
+
if (provider?.id !== GIT_PROVIDER_ID) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
const staged = provider.groups.find(group => group.id === STAGED_GROUP_ID);
|
|
151
|
+
return !!staged && staged.resources.length > 0;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -97,7 +97,7 @@ describe('AiConfigurationDetailWidget dispatch', () => {
|
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
|
-
* Pins the scrolling arithmetic of {@link AiConfigurationDetailWidget#
|
|
100
|
+
* Pins the scrolling arithmetic of {@link AiConfigurationDetailWidget#revealInBody} and, more importantly,
|
|
101
101
|
* that it writes only the scroll container.
|
|
102
102
|
*
|
|
103
103
|
* The predecessor was `element.scrollIntoView({ block: 'center' })`, which also scrolls every scrollable
|
|
@@ -106,7 +106,7 @@ describe('AiConfigurationDetailWidget dispatch', () => {
|
|
|
106
106
|
* `clientHeight` are stubbed; it also does not implement `scrollIntoView`, so reverting to it fails every
|
|
107
107
|
* assertion here (verified by patching the method back).
|
|
108
108
|
*/
|
|
109
|
-
describe('AiConfigurationDetailWidget
|
|
109
|
+
describe('AiConfigurationDetailWidget revealInBody', () => {
|
|
110
110
|
|
|
111
111
|
before(() => disableJSDOM = enableJSDOM());
|
|
112
112
|
after(() => disableJSDOM());
|
|
@@ -135,30 +135,30 @@ describe('AiConfigurationDetailWidget centerInBody', () => {
|
|
|
135
135
|
return { ancestor, body, element };
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
it('scrolls the container so the row
|
|
138
|
+
it('scrolls the container so the row sits at its top', () => {
|
|
139
139
|
const { body, element } = hierarchy(500);
|
|
140
|
-
// Row sits 400px below the body's top
|
|
141
|
-
widget.
|
|
142
|
-
expect(body.scrollTop).to.equal(
|
|
140
|
+
// Row sits 400px below the body's top, less the margin kept above it.
|
|
141
|
+
widget.revealInBody(body, element);
|
|
142
|
+
expect(body.scrollTop).to.equal(400 - AiConfigurationDetailWidget.TARGET_ROW_MARGIN);
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
-
it('leaves the scroll position alone when the row is already
|
|
146
|
-
const { body, element } = hierarchy(
|
|
147
|
-
widget.
|
|
145
|
+
it('leaves the scroll position alone when the row is already at the top', () => {
|
|
146
|
+
const { body, element } = hierarchy(100 + AiConfigurationDetailWidget.TARGET_ROW_MARGIN);
|
|
147
|
+
widget.revealInBody(body, element);
|
|
148
148
|
expect(body.scrollTop).to.equal(0);
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
-
it('scrolls back up for a row above the
|
|
151
|
+
it('scrolls back up for a row above the current position', () => {
|
|
152
152
|
const { body, element } = hierarchy(120);
|
|
153
|
-
// 20px below the body's top, so it has to scroll up
|
|
153
|
+
// 20px below the body's top, so it has to scroll up to put the row at the margin.
|
|
154
154
|
body.scrollTop = 300;
|
|
155
|
-
widget.
|
|
156
|
-
expect(body.scrollTop).to.equal(
|
|
155
|
+
widget.revealInBody(body, element);
|
|
156
|
+
expect(body.scrollTop).to.equal(300 + 20 - AiConfigurationDetailWidget.TARGET_ROW_MARGIN);
|
|
157
157
|
});
|
|
158
158
|
|
|
159
159
|
it('never scrolls an ancestor of the container', () => {
|
|
160
160
|
const { ancestor, body, element } = hierarchy(500);
|
|
161
|
-
widget.
|
|
161
|
+
widget.revealInBody(body, element);
|
|
162
162
|
expect(body.scrollTop, 'the container itself should have scrolled').to.not.equal(0);
|
|
163
163
|
expect(ancestor.scrollTop, 'scrolling an ancestor shifts the whole shell').to.equal(0);
|
|
164
164
|
});
|
|
@@ -33,6 +33,8 @@ export class AiConfigurationDetailWidget extends ReactWidget {
|
|
|
33
33
|
|
|
34
34
|
/** Frames a pending search highlight waits for its target row to be mounted before it is dropped. */
|
|
35
35
|
static readonly HIGHLIGHT_RETRY_FRAMES = 10;
|
|
36
|
+
/** Space kept above a row a deep link scrolled to, so that its section header stays visible. */
|
|
37
|
+
static readonly TARGET_ROW_MARGIN = 24;
|
|
36
38
|
|
|
37
39
|
@inject(AiConfigurationCategoryRegistry)
|
|
38
40
|
protected readonly registry: AiConfigurationCategoryRegistry;
|
|
@@ -201,7 +203,7 @@ export class AiConfigurationDetailWidget extends ReactWidget {
|
|
|
201
203
|
const element = body.querySelector<HTMLElement>(`[data-ai-config-row-id="${highlight.rowId}"]`);
|
|
202
204
|
if (element) {
|
|
203
205
|
this.pendingHighlight = undefined;
|
|
204
|
-
this.
|
|
206
|
+
this.revealInBody(body, element);
|
|
205
207
|
element.classList.add('ai-configuration-row-flash');
|
|
206
208
|
window.setTimeout(() => element.classList.remove('ai-configuration-row-flash'), 1200);
|
|
207
209
|
} else if (this.pendingHighlightFrames < AiConfigurationDetailWidget.HIGHLIGHT_RETRY_FRAMES) {
|
|
@@ -218,20 +220,23 @@ export class AiConfigurationDetailWidget extends ReactWidget {
|
|
|
218
220
|
}
|
|
219
221
|
|
|
220
222
|
/**
|
|
221
|
-
*
|
|
223
|
+
* Scrolls `body`, and nothing else, so that `element` sits at the top of it.
|
|
224
|
+
*
|
|
225
|
+
* Top rather than centered: a deep link lands on a page the user has not seen before, and a row in the
|
|
226
|
+
* middle of it reads as "the page opened somewhere", with the rows above it competing for the attention
|
|
227
|
+
* the flash is meant to direct. {@link TARGET_ROW_MARGIN} keeps the section header above the row visible.
|
|
222
228
|
*
|
|
223
229
|
* Deliberately not `Element.scrollIntoView`: that scrolls *every* scrollable ancestor to satisfy the
|
|
224
230
|
* requested alignment, and `overflow: hidden` does not prevent programmatic scrolling. When a row cannot
|
|
225
|
-
* be
|
|
226
|
-
* widget, which have no scrollbar for the user to scroll back
|
|
231
|
+
* be revealed inside the body alone, it therefore nudges the `overflow: hidden` containers above this
|
|
232
|
+
* widget, which have no scrollbar for the user to scroll back, shifting the whole shell, pushing the menu
|
|
227
233
|
* bar out of view and leaving a gap below the status bar.
|
|
228
234
|
*/
|
|
229
|
-
protected
|
|
235
|
+
protected revealInBody(body: HTMLElement, element: HTMLElement): void {
|
|
230
236
|
const bodyRect = body.getBoundingClientRect();
|
|
231
237
|
const elementRect = element.getBoundingClientRect();
|
|
232
238
|
const current = elementRect.top - bodyRect.top;
|
|
233
|
-
const centered = (body.clientHeight - elementRect.height) / 2;
|
|
234
239
|
// Assigning `scrollTop` clamps to the scrollable range, so short pages simply do not scroll.
|
|
235
|
-
body.scrollTop += current -
|
|
240
|
+
body.scrollTop += current - AiConfigurationDetailWidget.TARGET_ROW_MARGIN;
|
|
236
241
|
}
|
|
237
242
|
}
|
|
@@ -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 {
|
|
@@ -21,7 +21,7 @@ import { getAgentIconClass } from '../agent-icon';
|
|
|
21
21
|
|
|
22
22
|
/** A single navigable agent chip (icon + name); shared by the Variables, Skills and Slash Command surfaces. */
|
|
23
23
|
export const AgentChip: React.FC<{ agent: Agent; onOpenAgent: (agentId: string) => void }> = ({ agent, onOpenAgent }) => {
|
|
24
|
-
const label = nls.
|
|
24
|
+
const label = nls.localizeByDefault('Open agent {0}', agent.name);
|
|
25
25
|
return <button
|
|
26
26
|
type='button'
|
|
27
27
|
className='agent-chip'
|
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 {
|