@theia/ai-ide 1.76.0-next.6 → 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,341 @@
|
|
|
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 { nls } from '@theia/core';
|
|
18
|
+
import { codicon } from '@theia/core/lib/browser';
|
|
19
|
+
import * as React from '@theia/core/shared/react';
|
|
20
|
+
import { formatDistanceToNow } from 'date-fns';
|
|
21
|
+
import { compareModelsByRecency, LanguageModel } from '@theia/ai-core/lib/common';
|
|
22
|
+
import { FavoriteModelsService } from '@theia/ai-core/lib/browser';
|
|
23
|
+
import { DiscoveredModel, ModelDiscoveryAction, ModelDiscoveryStatus } from '@theia/ai-core/lib/common/model-discovery-status';
|
|
24
|
+
import { AiConfigurationItemStatus } from '@theia/ai-core-ui/lib/browser/ai-configuration/ai-configuration-category';
|
|
25
|
+
import {
|
|
26
|
+
AiConfigurationCallout, AiConfigurationEmptyState, AiConfigurationFilterInput, AiConfigurationFilterToggle, AiConfigurationIconButton, AiConfigurationSection,
|
|
27
|
+
AiConfigurationStatusBadge
|
|
28
|
+
} from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-configuration-primitives';
|
|
29
|
+
import { AiConfigurationItemRow } from '@theia/ai-core-ui/lib/browser/ai-configuration/components/ai-configuration-item-row';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Number of models from which the list gets a filter. Below it, scanning the list is faster than
|
|
33
|
+
* typing; above it — OpenAI reports dozens — it is not.
|
|
34
|
+
*/
|
|
35
|
+
const FILTER_THRESHOLD = 8;
|
|
36
|
+
|
|
37
|
+
export interface ProviderModelDiscoverySectionProps {
|
|
38
|
+
readonly status: ModelDiscoveryStatus;
|
|
39
|
+
/** The provider's currently registered models. */
|
|
40
|
+
readonly models: LanguageModel[];
|
|
41
|
+
/** Decides which models the pickers offer, and takes the user's checks. */
|
|
42
|
+
readonly favorites: FavoriteModelsService;
|
|
43
|
+
readonly onRefresh: () => void;
|
|
44
|
+
/** Runs what the provider offers to do about the current state, e.g. signing in to GitHub Copilot. */
|
|
45
|
+
readonly onAction: (action: ModelDiscoveryAction) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Renders a provider's model-discovery state (fetching / ready / cached / error / no credentials /
|
|
50
|
+
* awaiting consent / manual), a manual refresh, and the discovered models, below the provider's
|
|
51
|
+
* settings in the Models category. This is the provider-level surface that per-model status cannot
|
|
52
|
+
* give: fetching progress and failure of the list call itself.
|
|
53
|
+
*
|
|
54
|
+
* The list holds everything the provider offers, one entry per release included, which is more than
|
|
55
|
+
* a model picker can carry. Which of them the pickers offer is shown and controlled here, by the
|
|
56
|
+
* star on each row.
|
|
57
|
+
*/
|
|
58
|
+
export const ProviderModelDiscoverySection: React.FC<ProviderModelDiscoverySectionProps> = ({ status, models, favorites, onRefresh, onAction }) => {
|
|
59
|
+
const [filter, setFilter] = React.useState('');
|
|
60
|
+
const [favoritesOnly, setFavoritesOnly] = React.useState(false);
|
|
61
|
+
// Re-render when a star is set anywhere, and when a discovery changes what is featured.
|
|
62
|
+
const [favoritesRevision, setFavoritesRevision] = React.useState(0);
|
|
63
|
+
React.useEffect(() => {
|
|
64
|
+
const listener = favorites.onDidChange(() => setFavoritesRevision(revision => revision + 1));
|
|
65
|
+
return () => listener.dispose();
|
|
66
|
+
}, [favorites]);
|
|
67
|
+
const fetching = status.state === 'fetching';
|
|
68
|
+
const modelIdPrefix = status.modelIdPrefix ?? status.providerId;
|
|
69
|
+
const overridden = favorites.hasOverrides(modelIdPrefix);
|
|
70
|
+
// Without a credential there is nothing to ask the provider; refreshing would only report the same
|
|
71
|
+
// state back. The setting or the sign-in above the list is what moves this on.
|
|
72
|
+
const refreshable = status.state !== 'no-credentials' && status.state !== 'awaiting-consent';
|
|
73
|
+
// Newest first: a provider's recent models are what a list this long is scrolled for. Models whose
|
|
74
|
+
// provider reports no release date (Gemini reports none) fall back to alphabetical order.
|
|
75
|
+
const rows = React.useMemo(() => models
|
|
76
|
+
.slice()
|
|
77
|
+
.sort(compareModelsByRecency)
|
|
78
|
+
// `favoritesRevision` is not read here: it is what re-runs this when a star or the featured set changes.
|
|
79
|
+
.map(model => describeModel(model, status.discovered, favorites))
|
|
80
|
+
// The provider's current line-up first, newest first within each part. A list this long is read
|
|
81
|
+
// from the top, and the models the picker starts with are what it is usually opened for. The sort
|
|
82
|
+
// is stable, so it only lifts them past the older ones without disturbing the order.
|
|
83
|
+
.sort((left, right) => Number(right.featured) - Number(left.featured)),
|
|
84
|
+
[models, status.discovered, favorites, favoritesRevision]);
|
|
85
|
+
const matches = React.useMemo(
|
|
86
|
+
() => rows.filter(row => (!favoritesOnly || row.favorite) && matchesFilter(row, filter)),
|
|
87
|
+
[rows, filter, favoritesOnly]
|
|
88
|
+
);
|
|
89
|
+
const favoritesOnlyLabel = favoritesOnly
|
|
90
|
+
? nls.localize('theia/ai/ide/modelsConfiguration/showAllModels', 'Show all discovered models')
|
|
91
|
+
: nls.localize('theia/ai/ide/modelsConfiguration/showCheckedModels', 'Show only the models the AI chat input\'s model picker shows');
|
|
92
|
+
const filterToggles: AiConfigurationFilterToggle[] = [{
|
|
93
|
+
iconClass: codicon('pass'),
|
|
94
|
+
title: favoritesOnlyLabel,
|
|
95
|
+
active: favoritesOnly,
|
|
96
|
+
onToggle: () => setFavoritesOnly(!favoritesOnly)
|
|
97
|
+
}];
|
|
98
|
+
return <AiConfigurationSection
|
|
99
|
+
title={nls.localize('theia/ai/ide/modelsConfiguration/discovery', 'Model Discovery')}
|
|
100
|
+
count={rows.length > 0 ? rows.length : undefined}
|
|
101
|
+
subtitle={sectionSubtitle(status, rows.length, rows.filter(row => row.favorite).length)}
|
|
102
|
+
actions={<>
|
|
103
|
+
<AiConfigurationStatusBadge status={discoveryStatusBadge(status)} />
|
|
104
|
+
<AiConfigurationIconButton
|
|
105
|
+
iconClass={codicon(fetching ? 'sync' : 'refresh')}
|
|
106
|
+
title={refreshable
|
|
107
|
+
? nls.localize('theia/ai/ide/modelsConfiguration/refresh', 'Refresh the model list')
|
|
108
|
+
: status.message ?? nls.localize('theia/ai/ide/modelsConfiguration/refreshUnavailable', 'Nothing to refresh until the provider has a credential')}
|
|
109
|
+
busy={fetching}
|
|
110
|
+
disabled={fetching || !refreshable}
|
|
111
|
+
onClick={onRefresh}
|
|
112
|
+
/>
|
|
113
|
+
<AiConfigurationIconButton
|
|
114
|
+
iconClass={codicon('discard')}
|
|
115
|
+
title={overridden
|
|
116
|
+
? nls.localize('theia/ai/ide/modelsConfiguration/resetShown', 'Reset to defaults')
|
|
117
|
+
: nls.localize('theia/ai/ide/modelsConfiguration/resetShownUnavailable', 'Already at the defaults')}
|
|
118
|
+
disabled={!overridden}
|
|
119
|
+
onClick={() => favorites.resetToDefaults(modelIdPrefix)}
|
|
120
|
+
/>
|
|
121
|
+
</>}
|
|
122
|
+
>
|
|
123
|
+
<DiscoveryBody
|
|
124
|
+
status={status}
|
|
125
|
+
rows={rows}
|
|
126
|
+
matches={matches}
|
|
127
|
+
filter={filter}
|
|
128
|
+
onFilterChange={setFilter}
|
|
129
|
+
filterToggles={filterToggles}
|
|
130
|
+
favorites={favorites}
|
|
131
|
+
onRefresh={onRefresh}
|
|
132
|
+
onAction={onAction}
|
|
133
|
+
/>
|
|
134
|
+
</AiConfigurationSection>;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The line under the section title. A manually configured list says so, since that is why no discovery
|
|
139
|
+
* ran; otherwise it states once — rather than leaving it to the tooltip of each of forty rows — that
|
|
140
|
+
* the checks are about the chat input alone. An agent's model and a model alias are picked from every
|
|
141
|
+
* discovered model, on their own pages.
|
|
142
|
+
*/
|
|
143
|
+
function sectionSubtitle(status: ModelDiscoveryStatus, rowCount: number, shownCount: number): string | undefined {
|
|
144
|
+
if (status.state === 'overridden') {
|
|
145
|
+
return status.message;
|
|
146
|
+
}
|
|
147
|
+
// The ratio is the overview a list this long cannot give by itself, and it says what the checks mean.
|
|
148
|
+
return rowCount > 0
|
|
149
|
+
? nls.localize('theia/ai/ide/modelsConfiguration/shownExplanation',
|
|
150
|
+
'{0} of {1} shown in the AI chat input\'s model picker', shownCount, rowCount)
|
|
151
|
+
: undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** A registered model paired with whatever the provider reported about it. */
|
|
155
|
+
interface ModelRow {
|
|
156
|
+
readonly id: string;
|
|
157
|
+
/** Fully qualified id, i.e. with the `<provider>/` prefix, as the favorites are keyed. */
|
|
158
|
+
readonly qualifiedId: string;
|
|
159
|
+
readonly label?: string;
|
|
160
|
+
readonly description?: string;
|
|
161
|
+
readonly released?: number;
|
|
162
|
+
readonly ready: boolean;
|
|
163
|
+
readonly statusMessage?: string;
|
|
164
|
+
/** Whether the model pickers offer this model. */
|
|
165
|
+
readonly favorite: boolean;
|
|
166
|
+
/** Whether it is offered automatically, in which case the star cannot be removed. */
|
|
167
|
+
readonly featured: boolean;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function describeModel(model: LanguageModel, discovered: readonly DiscoveredModel[] | undefined, favorites: FavoriteModelsService): ModelRow {
|
|
171
|
+
// Registered ids carry the `<provider>/` prefix; what the provider reported does not.
|
|
172
|
+
const id = model.id.substring(model.id.indexOf('/') + 1);
|
|
173
|
+
const reported = discovered?.find(candidate => candidate.id === id);
|
|
174
|
+
return {
|
|
175
|
+
id,
|
|
176
|
+
qualifiedId: model.id,
|
|
177
|
+
label: reported?.label ?? model.name,
|
|
178
|
+
description: reported?.description,
|
|
179
|
+
released: model.released ?? reported?.released,
|
|
180
|
+
ready: model.status.status === 'ready',
|
|
181
|
+
statusMessage: model.status.message,
|
|
182
|
+
favorite: favorites.isFavorite(model.id),
|
|
183
|
+
featured: favorites.isFeatured(model.id)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function matchesFilter(row: ModelRow, filter: string): boolean {
|
|
188
|
+
const query = filter.trim().toLowerCase();
|
|
189
|
+
return query.length === 0
|
|
190
|
+
|| row.id.toLowerCase().includes(query)
|
|
191
|
+
|| (row.label?.toLowerCase().includes(query) ?? false);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
interface DiscoveryBodyProps {
|
|
195
|
+
readonly status: ModelDiscoveryStatus;
|
|
196
|
+
readonly rows: ModelRow[];
|
|
197
|
+
readonly matches: ModelRow[];
|
|
198
|
+
readonly filter: string;
|
|
199
|
+
readonly onFilterChange: (value: string) => void;
|
|
200
|
+
/** Narrowings offered inside the filter box, on top of the typed text. */
|
|
201
|
+
readonly filterToggles: AiConfigurationFilterToggle[];
|
|
202
|
+
readonly favorites: FavoriteModelsService;
|
|
203
|
+
readonly onRefresh: () => void;
|
|
204
|
+
readonly onAction: (action: ModelDiscoveryAction) => void;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const DiscoveryBody: React.FC<DiscoveryBodyProps> = ({ status, rows, matches, filter, onFilterChange, filterToggles, favorites, onRefresh, onAction }) => {
|
|
208
|
+
// The provider's API-key and consent settings sit on the same page, right above this section — but
|
|
209
|
+
// a provider whose credential is a sign-in rather than a setting names the command that gets one.
|
|
210
|
+
if (status.state === 'no-credentials' || status.state === 'awaiting-consent') {
|
|
211
|
+
return <AiConfigurationEmptyState
|
|
212
|
+
message={status.message ?? ''}
|
|
213
|
+
iconClass={codicon('key')}
|
|
214
|
+
action={status.action && <button className='theia-button main' onClick={() => onAction(status.action!)}>{status.action.label}</button>}
|
|
215
|
+
/>;
|
|
216
|
+
}
|
|
217
|
+
const retry = <button className='theia-button main' onClick={onRefresh}>{nls.localizeByDefault('Retry')}</button>;
|
|
218
|
+
const refresh = <button className='theia-button main' onClick={onRefresh}>{nls.localizeByDefault('Refresh')}</button>;
|
|
219
|
+
return <>
|
|
220
|
+
{/* A failed refresh does not unregister what an earlier one found, so the models stay listed
|
|
221
|
+
below the failure rather than being replaced by it. */}
|
|
222
|
+
{status.state === 'error' && <AiConfigurationCallout message={status.message ?? ''} action={retry} />}
|
|
223
|
+
{status.state === 'ready' && status.fromCache && status.message && <AiConfigurationCallout message={status.message} action={refresh} />}
|
|
224
|
+
{rows.length === 0
|
|
225
|
+
? <AiConfigurationEmptyState
|
|
226
|
+
message={nls.localize('theia/ai/ide/modelsConfiguration/noModels', 'No models discovered yet.')}
|
|
227
|
+
iconClass={codicon('cloud')}
|
|
228
|
+
/>
|
|
229
|
+
: <>
|
|
230
|
+
{rows.length > FILTER_THRESHOLD && <AiConfigurationFilterInput
|
|
231
|
+
value={filter}
|
|
232
|
+
onChange={onFilterChange}
|
|
233
|
+
placeholder={nls.localize('theia/ai/ide/modelsConfiguration/filterModels', 'Filter models')}
|
|
234
|
+
toggles={filterToggles}
|
|
235
|
+
/>}
|
|
236
|
+
{matches.length === 0
|
|
237
|
+
? <AiConfigurationEmptyState message={nls.localizeByDefault('No results found')} />
|
|
238
|
+
: <ModelRows rows={matches} favorites={favorites} />}
|
|
239
|
+
</>}
|
|
240
|
+
</>;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const ModelRows: React.FC<{ rows: ModelRow[]; favorites: FavoriteModelsService }> = ({ rows, favorites }) => <>
|
|
244
|
+
{rows.map(row => <AiConfigurationItemRow
|
|
245
|
+
key={row.id}
|
|
246
|
+
// The id is the heading on every row, whatever the provider reports: it is what agents, aliases
|
|
247
|
+
// and settings reference, and OpenAI reports nothing else, so heading it with the name where
|
|
248
|
+
// there is one would give each provider a differently shaped list.
|
|
249
|
+
label={row.id}
|
|
250
|
+
// Kept on the row whether the model is checked or not: it says that this is one of the
|
|
251
|
+
// provider's current models, which is why the picker starts with it and stays true after
|
|
252
|
+
// someone unchecks it.
|
|
253
|
+
tags={row.featured
|
|
254
|
+
? [{
|
|
255
|
+
label: nls.localizeByDefault('Default'),
|
|
256
|
+
title: nls.localize('theia/ai/ide/modelsConfiguration/defaultModel',
|
|
257
|
+
'One of the newest models of this provider, which the AI chat input\'s model picker shows unless it is hidden here')
|
|
258
|
+
}]
|
|
259
|
+
: undefined}
|
|
260
|
+
description={modelRowDescription(row)}
|
|
261
|
+
// Every model in this list is ready as long as the provider has a key, which it has whenever the
|
|
262
|
+
// list is shown at all. Only the exception is worth a badge.
|
|
263
|
+
status={row.ready ? undefined : notReadyBadge(row)}
|
|
264
|
+
trailing={<>
|
|
265
|
+
{/* Names the order the list is in, which is otherwise a claim the user cannot check. */}
|
|
266
|
+
{row.released !== undefined && <span className='ai-configuration-item-row-detail'>
|
|
267
|
+
{nls.localize('theia/ai/ide/modelsConfiguration/released', 'released {0}',
|
|
268
|
+
formatDistanceToNow(row.released, { addSuffix: true }))}
|
|
269
|
+
</span>}
|
|
270
|
+
<ShowInChatButton row={row} favorites={favorites} />
|
|
271
|
+
</>}
|
|
272
|
+
/>)}
|
|
273
|
+
</>;
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Whether the AI chat input's model picker shows this model: a checked circle when it does, an empty
|
|
277
|
+
* one when it does not. Nothing else is affected — an agent's model and a model alias are picked from
|
|
278
|
+
* every discovered model, on their own pages.
|
|
279
|
+
*
|
|
280
|
+
* What a model released tomorrow is shown by is the `Default` badge on its row, not a check made
|
|
281
|
+
* here; unchecking one of those is what says otherwise, and that is all that is stored.
|
|
282
|
+
*/
|
|
283
|
+
const ShowInChatButton: React.FC<{ row: ModelRow; favorites: FavoriteModelsService }> = ({ row, favorites }) => {
|
|
284
|
+
const title = row.favorite
|
|
285
|
+
? nls.localize('theia/ai/ide/modelsConfiguration/hideModel', 'Hide this model from the AI chat input\'s model picker')
|
|
286
|
+
: nls.localize('theia/ai/ide/modelsConfiguration/showModel', 'Show this model in the AI chat input\'s model picker');
|
|
287
|
+
return <AiConfigurationIconButton
|
|
288
|
+
iconClass={codicon(row.favorite ? 'pass' : 'circle-large')}
|
|
289
|
+
title={title}
|
|
290
|
+
onClick={() => favorites.toggleFavorite(row.qualifiedId)}
|
|
291
|
+
/>;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
/** What the provider reports about the model, if anything: its name, its description, or both. */
|
|
295
|
+
function modelRowDescription(row: ModelRow): string | undefined {
|
|
296
|
+
const parts = [row.label, row.description].filter(part => part !== undefined);
|
|
297
|
+
return parts.length > 0 ? parts.join(' · ') : undefined;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function notReadyBadge(row: ModelRow): AiConfigurationItemStatus {
|
|
301
|
+
return {
|
|
302
|
+
kind: 'warn',
|
|
303
|
+
label: nls.localize('theia/ai/ide/modelsConfiguration/modelNotReady', 'Not ready'),
|
|
304
|
+
tooltip: row.statusMessage
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Maps the discovery state onto the shared status badge, so it reads like every other badge in the view. */
|
|
309
|
+
function discoveryStatusBadge(status: ModelDiscoveryStatus): AiConfigurationItemStatus {
|
|
310
|
+
const badge = genericStatusBadge(status);
|
|
311
|
+
// A provider whose credential is not an API key says so itself; the state alone cannot know.
|
|
312
|
+
return status.stateLabel ? { ...badge, label: status.stateLabel } : badge;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function genericStatusBadge(status: ModelDiscoveryStatus): AiConfigurationItemStatus {
|
|
316
|
+
switch (status.state) {
|
|
317
|
+
case 'no-credentials':
|
|
318
|
+
return { kind: 'warn', label: nls.localize('theia/ai/ide/modelsConfiguration/noKey', 'No API key'), tooltip: status.message };
|
|
319
|
+
case 'awaiting-consent':
|
|
320
|
+
return { kind: 'warn', label: nls.localize('theia/ai/ide/modelsConfiguration/awaitingConsent', 'Awaiting consent'), tooltip: status.message };
|
|
321
|
+
case 'fetching':
|
|
322
|
+
return { kind: 'off', label: nls.localize('theia/ai/ide/modelsConfiguration/fetching', 'Fetching…'), tooltip: status.message };
|
|
323
|
+
case 'error':
|
|
324
|
+
return { kind: 'error', label: nls.localizeByDefault('Error'), tooltip: status.message };
|
|
325
|
+
case 'overridden':
|
|
326
|
+
return { kind: 'off', label: nls.localizeByDefault('Manual'), tooltip: status.message };
|
|
327
|
+
case 'ready':
|
|
328
|
+
if (status.fromCache) {
|
|
329
|
+
return { kind: 'warn', label: nls.localizeByDefault('Cached'), tooltip: status.message };
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
kind: 'on',
|
|
333
|
+
label: status.lastFetch
|
|
334
|
+
? nls.localize('theia/ai/ide/modelsConfiguration/updated', 'Updated {0}', formatDistanceToNow(status.lastFetch, { addSuffix: true }))
|
|
335
|
+
: nls.localize('theia/ai/ide/modelsConfiguration/ready', 'Ready'),
|
|
336
|
+
tooltip: status.message
|
|
337
|
+
};
|
|
338
|
+
default:
|
|
339
|
+
return { kind: 'off', label: nls.localizeByDefault('Idle'), tooltip: status.message };
|
|
340
|
+
}
|
|
341
|
+
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
Agent,
|
|
19
19
|
AISettingsService,
|
|
20
20
|
FrontendLanguageModelRegistry,
|
|
21
|
+
groupModelsByProvider,
|
|
21
22
|
LanguageModel,
|
|
22
23
|
LanguageModelRequirement,
|
|
23
24
|
PREFERENCE_NAME_REASONING,
|
|
@@ -248,13 +249,20 @@ export const LanguageModelRenderer: React.FC<LanguageModelSettingsProps> = (
|
|
|
248
249
|
label: nls.localize('theia/ai/core/languageModelRenderer/alias', '[alias] {0}', alias.id)
|
|
249
250
|
});
|
|
250
251
|
});
|
|
251
|
-
|
|
252
|
+
const modelOption = (model: LanguageModel): SelectOption => {
|
|
252
253
|
const isNotReady = model.status.status !== 'ready';
|
|
253
|
-
|
|
254
|
+
return {
|
|
254
255
|
value: model.id,
|
|
255
256
|
label: `${model.name ?? model.id} ${isNotReady ? '✗' : '✓'}`,
|
|
256
257
|
description: isNotReady && model.status.message ? model.status.message : undefined
|
|
257
|
-
}
|
|
258
|
+
};
|
|
259
|
+
};
|
|
260
|
+
// Divided per provider, newest first inside each group. Every model is offered here, not just
|
|
261
|
+
// the ones the chat picker carries: this is the page where a specific older or release-pinned
|
|
262
|
+
// model is deliberately assigned to an agent.
|
|
263
|
+
groupModelsByProvider(languageModels ?? []).forEach(({ models }) => {
|
|
264
|
+
options.push({ separator: true });
|
|
265
|
+
options.push(...models.map(modelOption));
|
|
258
266
|
});
|
|
259
267
|
return options;
|
|
260
268
|
}, [aliases, languageModels]);
|
|
@@ -0,0 +1,33 @@
|
|
|
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 { Command } from '@theia/core/lib/common/command';
|
|
18
|
+
|
|
19
|
+
/** Opens the AI getting started walkthrough, from the chat view, a menu or the command palette. */
|
|
20
|
+
export const AI_OPEN_GETTING_STARTED_WALKTHROUGH = Command.toLocalizedCommand({
|
|
21
|
+
id: 'ai.openGettingStartedWalkthrough',
|
|
22
|
+
label: 'Get Started with AI'
|
|
23
|
+
}, 'theia/ai/ide/openGettingStartedWalkthrough');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sets the default chat agent to the agent id passed as the first argument.
|
|
27
|
+
* Lets the walkthrough offer the recommended agents as one click each, rather than sending the user to the
|
|
28
|
+
* preference row to pick one there.
|
|
29
|
+
*/
|
|
30
|
+
export const AI_SET_DEFAULT_CHAT_AGENT = Command.toLocalizedCommand({
|
|
31
|
+
id: 'ai.setDefaultChatAgent',
|
|
32
|
+
label: 'Set Default Chat Agent'
|
|
33
|
+
}, 'theia/ai/ide/setDefaultChatAgent');
|
|
@@ -0,0 +1,64 @@
|
|
|
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 { CommandContribution, CommandRegistry } from '@theia/core/lib/common/command';
|
|
18
|
+
import { PreferenceScope, PreferenceService } from '@theia/core/lib/common/preferences';
|
|
19
|
+
import { MessageService } from '@theia/core/lib/common/message-service';
|
|
20
|
+
import { nls } from '@theia/core/lib/common/nls';
|
|
21
|
+
import { ChatAgentService } from '@theia/ai-chat/lib/common';
|
|
22
|
+
import { DEFAULT_CHAT_AGENT_PREF } from '@theia/ai-chat/lib/common/ai-chat-preferences';
|
|
23
|
+
import { WalkthroughCommands } from '@theia/getting-started/lib/common/walkthrough-commands';
|
|
24
|
+
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
25
|
+
import { AI_GETTING_STARTED_WALKTHROUGH_ID } from './ai-getting-started-walkthrough';
|
|
26
|
+
import { AI_OPEN_GETTING_STARTED_WALKTHROUGH, AI_SET_DEFAULT_CHAT_AGENT } from './ai-getting-started-commands';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Opens the AI getting started walkthrough from anywhere - the chat view, the command palette or a menu -
|
|
30
|
+
* without every caller having to know the walkthrough id.
|
|
31
|
+
*/
|
|
32
|
+
@injectable()
|
|
33
|
+
export class AiGettingStartedContribution implements CommandContribution {
|
|
34
|
+
|
|
35
|
+
@inject(PreferenceService)
|
|
36
|
+
protected readonly preferenceService: PreferenceService;
|
|
37
|
+
|
|
38
|
+
@inject(MessageService)
|
|
39
|
+
protected readonly messageService: MessageService;
|
|
40
|
+
|
|
41
|
+
@inject(ChatAgentService)
|
|
42
|
+
protected readonly chatAgentService: ChatAgentService;
|
|
43
|
+
|
|
44
|
+
registerCommands(commands: CommandRegistry): void {
|
|
45
|
+
// Deliberately not gated on the AI features being enabled: this is where they get enabled.
|
|
46
|
+
commands.registerCommand(AI_OPEN_GETTING_STARTED_WALKTHROUGH, {
|
|
47
|
+
execute: () => commands.executeCommand(WalkthroughCommands.OPEN_WALKTHROUGH.id, AI_GETTING_STARTED_WALKTHROUGH_ID)
|
|
48
|
+
});
|
|
49
|
+
commands.registerCommand(AI_SET_DEFAULT_CHAT_AGENT, {
|
|
50
|
+
execute: (agentId: string) => this.setDefaultChatAgent(agentId),
|
|
51
|
+
isEnabled: (agentId: unknown) => typeof agentId === 'string' && !!agentId
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Writes the default chat agent preference and says so: the walkthrough offers this as a button, and
|
|
57
|
+
* without the message the only feedback would be the step ticking itself off further up the page.
|
|
58
|
+
*/
|
|
59
|
+
protected async setDefaultChatAgent(agentId: string): Promise<void> {
|
|
60
|
+
await this.preferenceService.set(DEFAULT_CHAT_AGENT_PREF, agentId, PreferenceScope.User);
|
|
61
|
+
const label = this.chatAgentService.getAgent(agentId)?.name ?? agentId;
|
|
62
|
+
this.messageService.info(nls.localize('theia/ai/ide/defaultChatAgentSet', 'The default chat agent is now {0}.', label));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
|
18
|
+
let disableJSDOM = enableJSDOM();
|
|
19
|
+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
|
20
|
+
FrontendApplicationConfigProvider.set({});
|
|
21
|
+
import { expect } from 'chai';
|
|
22
|
+
import { ENABLE_AI_CONTEXT_KEY } from '@theia/ai-core/lib/browser/ai-activation-service';
|
|
23
|
+
import { WalkthroughDefinition } from '@theia/getting-started/lib/common/walkthrough-types';
|
|
24
|
+
import { AI_GETTING_STARTED_WALKTHROUGH_ID, AiGettingStartedWalkthroughProvider } from './ai-getting-started-walkthrough';
|
|
25
|
+
import {
|
|
26
|
+
AI_CHAT_USED_CONTEXT_KEY,
|
|
27
|
+
AI_DEFAULT_AGENT_CONTEXT_KEY,
|
|
28
|
+
AI_MODEL_READY_CONTEXT_KEY,
|
|
29
|
+
AI_TOOL_CONFIRMATION_CONFIGURED_CONTEXT_KEY
|
|
30
|
+
} from './ai-walkthrough-context-keys';
|
|
31
|
+
disableJSDOM();
|
|
32
|
+
|
|
33
|
+
/** The commands the walkthrough is allowed to link to; each one is registered by one of the AI packages. */
|
|
34
|
+
const KNOWN_COMMANDS = new Set([
|
|
35
|
+
'aiConfiguration:open',
|
|
36
|
+
'aiConfiguration:openTools',
|
|
37
|
+
'aiChat:toggle',
|
|
38
|
+
'workspace:manageTrust',
|
|
39
|
+
'perspective.switch',
|
|
40
|
+
'ai.setDefaultChatAgent'
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const KNOWN_CONTEXT_KEYS = new Set([
|
|
44
|
+
ENABLE_AI_CONTEXT_KEY,
|
|
45
|
+
AI_MODEL_READY_CONTEXT_KEY,
|
|
46
|
+
AI_CHAT_USED_CONTEXT_KEY,
|
|
47
|
+
AI_DEFAULT_AGENT_CONTEXT_KEY,
|
|
48
|
+
AI_TOOL_CONFIRMATION_CONFIGURED_CONTEXT_KEY
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
describe('AiGettingStartedWalkthroughProvider', () => {
|
|
52
|
+
|
|
53
|
+
let walkthrough: WalkthroughDefinition;
|
|
54
|
+
|
|
55
|
+
/** The recommended agents a product contributes, which the agent step turns into one link each. */
|
|
56
|
+
const recommendedAgents = [
|
|
57
|
+
{ id: 'Coder', label: 'Coder', description: 'Code generation and modification' },
|
|
58
|
+
{ id: 'Architect', label: 'Architect', description: 'High-level design and architecture' }
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
function createProvider(): AiGettingStartedWalkthroughProvider {
|
|
62
|
+
const provider = new AiGettingStartedWalkthroughProvider();
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
(provider as any).recommendationService = { getRecommendedAgents: () => recommendedAgents };
|
|
65
|
+
return provider;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function agentStepOf(provider: AiGettingStartedWalkthroughProvider): string {
|
|
69
|
+
return provider.getWalkthroughs()[0].steps.find(step => step.id === 'choose-agent')!.description;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
before(() => {
|
|
73
|
+
disableJSDOM = enableJSDOM();
|
|
74
|
+
FrontendApplicationConfigProvider.set({});
|
|
75
|
+
walkthrough = createProvider().getWalkthroughs()[0];
|
|
76
|
+
});
|
|
77
|
+
after(() => disableJSDOM());
|
|
78
|
+
|
|
79
|
+
it('should contribute the getting started walkthrough', () => {
|
|
80
|
+
const walkthroughs = createProvider().getWalkthroughs();
|
|
81
|
+
|
|
82
|
+
expect(walkthroughs).to.have.lengthOf(1);
|
|
83
|
+
expect(walkthroughs[0].id).to.equal(AI_GETTING_STARTED_WALKTHROUGH_ID);
|
|
84
|
+
expect(walkthroughs[0].steps.map(step => step.id)).to.deep.equal(
|
|
85
|
+
['ai-support', 'enable-ai', 'connect-model', 'choose-agent', 'first-question', 'tool-confirmation', 'go-further']);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should offer every recommended agent as an action that sets it as the default', () => {
|
|
89
|
+
const description = agentStepOf(createProvider());
|
|
90
|
+
for (const agent of recommendedAgents) {
|
|
91
|
+
// Alone in its paragraph, which is what the welcome page renders as a button, and carrying the
|
|
92
|
+
// agent's description as the link title so it stays available as a tooltip.
|
|
93
|
+
expect(description, `action for ${agent.id}`).to.include(
|
|
94
|
+
`\n\n[@${agent.label}](command:ai.setDefaultChatAgent?${encodeURIComponent(JSON.stringify(agent.id))} "${agent.description}")`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should keep the preference row reachable', () => {
|
|
99
|
+
expect(agentStepOf(createProvider())).to.include('command:aiConfiguration:open?ai-features.chat.defaultChatAgent');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should give every step an id, a title and a description', () => {
|
|
103
|
+
const ids = walkthrough.steps.map(step => step.id);
|
|
104
|
+
expect(new Set(ids).size).to.equal(ids.length);
|
|
105
|
+
for (const step of walkthrough.steps) {
|
|
106
|
+
expect(step.title, `title of ${step.id}`).to.not.be.empty;
|
|
107
|
+
expect(step.description, `description of ${step.id}`).to.not.be.empty;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should substitute every placeholder', () => {
|
|
112
|
+
for (const step of walkthrough.steps) {
|
|
113
|
+
expect(step.title, `title of ${step.id}`).to.not.match(/\{\d+\}/);
|
|
114
|
+
expect(step.description, `description of ${step.id}`).to.not.match(/\{\d+\}/);
|
|
115
|
+
}
|
|
116
|
+
expect(walkthrough.description).to.not.match(/\{\d+\}/);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should only link to commands that exist', () => {
|
|
120
|
+
for (const step of walkthrough.steps) {
|
|
121
|
+
for (const match of step.description.matchAll(/\(command:([^?)]+)/g)) {
|
|
122
|
+
expect(KNOWN_COMMANDS, `command of ${step.id}`).to.include(match[1]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should only complete on context keys that are published, so that steps also tick for an already configured user', () => {
|
|
128
|
+
for (const step of walkthrough.steps) {
|
|
129
|
+
for (const event of step.completionEvents ?? []) {
|
|
130
|
+
if (event.startsWith('onContext:')) {
|
|
131
|
+
expect(KNOWN_CONTEXT_KEYS, `context key of ${step.id}`).to.include(event.substring('onContext:'.length));
|
|
132
|
+
} else {
|
|
133
|
+
// An `onLink:` event only ever fires for a link the step actually offers.
|
|
134
|
+
expect(event, `completion event of ${step.id}`).to.match(/^onLink:/);
|
|
135
|
+
expect(step.description, `link of ${step.id}`).to.include(`(${event.substring('onLink:'.length)})`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should have a completion event for every step that can be verified', () => {
|
|
142
|
+
const withoutCompletion = walkthrough.steps.filter(step => !step.completionEvents?.length).map(step => step.id);
|
|
143
|
+
// The closing step only points at further reading, so there is nothing to observe; it is marked done by hand.
|
|
144
|
+
expect(withoutCompletion).to.deep.equal(['go-further']);
|
|
145
|
+
});
|
|
146
|
+
});
|