@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.
Files changed (174) hide show
  1. package/lib/browser/ai-allow-all-mode-chat-banner.js +1 -1
  2. package/lib/browser/ai-allow-all-mode-chat-banner.js.map +1 -1
  3. package/lib/browser/ai-aware-scm-commit-widget.d.ts +27 -0
  4. package/lib/browser/ai-aware-scm-commit-widget.d.ts.map +1 -0
  5. package/lib/browser/ai-aware-scm-commit-widget.js +142 -0
  6. package/lib/browser/ai-aware-scm-commit-widget.js.map +1 -0
  7. package/lib/browser/ai-configuration/ai-configuration-detail-widget.d.ts +10 -4
  8. package/lib/browser/ai-configuration/ai-configuration-detail-widget.d.ts.map +1 -1
  9. package/lib/browser/ai-configuration/ai-configuration-detail-widget.js +12 -7
  10. package/lib/browser/ai-configuration/ai-configuration-detail-widget.js.map +1 -1
  11. package/lib/browser/ai-configuration/ai-configuration-detail-widget.spec.js +14 -14
  12. package/lib/browser/ai-configuration/ai-configuration-detail-widget.spec.js.map +1 -1
  13. package/lib/browser/ai-configuration/ai-configuration-tree-widget.d.ts +6 -0
  14. package/lib/browser/ai-configuration/ai-configuration-tree-widget.d.ts.map +1 -1
  15. package/lib/browser/ai-configuration/ai-configuration-tree-widget.js +9 -0
  16. package/lib/browser/ai-configuration/ai-configuration-tree-widget.js.map +1 -1
  17. package/lib/browser/ai-configuration/categories/agent-capabilities-settings.d.ts.map +1 -1
  18. package/lib/browser/ai-configuration/categories/agent-capabilities-settings.js +2 -1
  19. package/lib/browser/ai-configuration/categories/agent-capabilities-settings.js.map +1 -1
  20. package/lib/browser/ai-configuration/categories/agent-chips.js +1 -1
  21. package/lib/browser/ai-configuration/categories/agent-chips.js.map +1 -1
  22. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.d.ts +8 -0
  23. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.d.ts.map +1 -1
  24. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.js +27 -8
  25. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.js.map +1 -1
  26. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.spec.js +38 -2
  27. package/lib/browser/ai-configuration/categories/model-aliases-configuration-category.spec.js.map +1 -1
  28. package/lib/browser/ai-configuration/categories/models-configuration-category.d.ts +13 -2
  29. package/lib/browser/ai-configuration/categories/models-configuration-category.d.ts.map +1 -1
  30. package/lib/browser/ai-configuration/categories/models-configuration-category.js +41 -2
  31. package/lib/browser/ai-configuration/categories/models-configuration-category.js.map +1 -1
  32. package/lib/browser/ai-configuration/categories/models-configuration-category.spec.js +32 -2
  33. package/lib/browser/ai-configuration/categories/models-configuration-category.spec.js.map +1 -1
  34. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.d.ts +26 -0
  35. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.d.ts.map +1 -0
  36. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.js +212 -0
  37. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.js.map +1 -0
  38. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.d.ts +2 -0
  39. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.d.ts.map +1 -0
  40. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.js +423 -0
  41. package/lib/browser/ai-configuration/categories/provider-model-discovery-section.spec.js.map +1 -0
  42. package/lib/browser/ai-configuration/language-model-renderer.d.ts.map +1 -1
  43. package/lib/browser/ai-configuration/language-model-renderer.js +10 -3
  44. package/lib/browser/ai-configuration/language-model-renderer.js.map +1 -1
  45. package/lib/browser/ai-getting-started-commands.d.ts +10 -0
  46. package/lib/browser/ai-getting-started-commands.d.ts.map +1 -0
  47. package/lib/browser/ai-getting-started-commands.js +34 -0
  48. package/lib/browser/ai-getting-started-commands.js.map +1 -0
  49. package/lib/browser/ai-getting-started-contribution.d.ts +20 -0
  50. package/lib/browser/ai-getting-started-contribution.d.ts.map +1 -0
  51. package/lib/browser/ai-getting-started-contribution.js +70 -0
  52. package/lib/browser/ai-getting-started-contribution.js.map +1 -0
  53. package/lib/browser/ai-getting-started-walkthrough.d.ts +41 -0
  54. package/lib/browser/ai-getting-started-walkthrough.d.ts.map +1 -0
  55. package/lib/browser/ai-getting-started-walkthrough.js +225 -0
  56. package/lib/browser/ai-getting-started-walkthrough.js.map +1 -0
  57. package/lib/browser/ai-getting-started-walkthrough.spec.d.ts +2 -0
  58. package/lib/browser/ai-getting-started-walkthrough.spec.d.ts.map +1 -0
  59. package/lib/browser/ai-getting-started-walkthrough.spec.js +124 -0
  60. package/lib/browser/ai-getting-started-walkthrough.spec.js.map +1 -0
  61. package/lib/browser/ai-walkthrough-context-keys.d.ts +55 -0
  62. package/lib/browser/ai-walkthrough-context-keys.d.ts.map +1 -0
  63. package/lib/browser/ai-walkthrough-context-keys.js +145 -0
  64. package/lib/browser/ai-walkthrough-context-keys.js.map +1 -0
  65. package/lib/browser/ai-walkthrough-context-keys.spec.d.ts +2 -0
  66. package/lib/browser/ai-walkthrough-context-keys.spec.d.ts.map +1 -0
  67. package/lib/browser/ai-walkthrough-context-keys.spec.js +166 -0
  68. package/lib/browser/ai-walkthrough-context-keys.spec.js.map +1 -0
  69. package/lib/browser/chat-session-item.js +2 -2
  70. package/lib/browser/chat-session-item.js.map +1 -1
  71. package/lib/browser/commit-message-agent.d.ts +31 -0
  72. package/lib/browser/commit-message-agent.d.ts.map +1 -0
  73. package/lib/browser/commit-message-agent.js +99 -0
  74. package/lib/browser/commit-message-agent.js.map +1 -0
  75. package/lib/browser/commit-message-agent.spec.d.ts +2 -0
  76. package/lib/browser/commit-message-agent.spec.d.ts.map +1 -0
  77. package/lib/browser/commit-message-agent.spec.js +82 -0
  78. package/lib/browser/commit-message-agent.spec.js.map +1 -0
  79. package/lib/browser/commit-message-command-contribution.d.ts +15 -0
  80. package/lib/browser/commit-message-command-contribution.d.ts.map +1 -0
  81. package/lib/browser/commit-message-command-contribution.js +73 -0
  82. package/lib/browser/commit-message-command-contribution.js.map +1 -0
  83. package/lib/browser/commit-message-commands.d.ts +5 -0
  84. package/lib/browser/commit-message-commands.d.ts.map +1 -0
  85. package/lib/browser/commit-message-commands.js +28 -0
  86. package/lib/browser/commit-message-commands.js.map +1 -0
  87. package/lib/browser/commit-message-prompt-template.d.ts +11 -0
  88. package/lib/browser/commit-message-prompt-template.d.ts.map +1 -0
  89. package/lib/browser/commit-message-prompt-template.js +63 -0
  90. package/lib/browser/commit-message-prompt-template.js.map +1 -0
  91. package/lib/browser/commit-message-runner.d.ts +41 -0
  92. package/lib/browser/commit-message-runner.d.ts.map +1 -0
  93. package/lib/browser/commit-message-runner.js +143 -0
  94. package/lib/browser/commit-message-runner.js.map +1 -0
  95. package/lib/browser/commit-message-runner.spec.d.ts +2 -0
  96. package/lib/browser/commit-message-runner.spec.d.ts.map +1 -0
  97. package/lib/browser/commit-message-runner.spec.js +206 -0
  98. package/lib/browser/commit-message-runner.spec.js.map +1 -0
  99. package/lib/browser/frontend-module.d.ts.map +1 -1
  100. package/lib/browser/frontend-module.js +25 -0
  101. package/lib/browser/frontend-module.js.map +1 -1
  102. package/lib/browser/git-changes-tool.d.ts +73 -0
  103. package/lib/browser/git-changes-tool.d.ts.map +1 -0
  104. package/lib/browser/git-changes-tool.js +217 -0
  105. package/lib/browser/git-changes-tool.js.map +1 -0
  106. package/lib/browser/git-changes-tool.spec.d.ts +2 -0
  107. package/lib/browser/git-changes-tool.spec.d.ts.map +1 -0
  108. package/lib/browser/git-changes-tool.spec.js +167 -0
  109. package/lib/browser/git-changes-tool.spec.js.map +1 -0
  110. package/lib/browser/ide-chat-welcome-message-provider.d.ts +26 -28
  111. package/lib/browser/ide-chat-welcome-message-provider.d.ts.map +1 -1
  112. package/lib/browser/ide-chat-welcome-message-provider.js +61 -151
  113. package/lib/browser/ide-chat-welcome-message-provider.js.map +1 -1
  114. package/lib/browser/review/pr-review-capability-contribution.js +1 -1
  115. package/lib/browser/review/pr-review-capability-contribution.js.map +1 -1
  116. package/lib/browser/user-interaction-tool-renderer.d.ts +6 -0
  117. package/lib/browser/user-interaction-tool-renderer.d.ts.map +1 -1
  118. package/lib/browser/user-interaction-tool-renderer.js +36 -7
  119. package/lib/browser/user-interaction-tool-renderer.js.map +1 -1
  120. package/lib/browser/user-interaction-tool.d.ts +23 -1
  121. package/lib/browser/user-interaction-tool.d.ts.map +1 -1
  122. package/lib/browser/user-interaction-tool.js +56 -17
  123. package/lib/browser/user-interaction-tool.js.map +1 -1
  124. package/lib/browser/user-interaction-tool.spec.js +151 -2
  125. package/lib/browser/user-interaction-tool.spec.js.map +1 -1
  126. package/lib/common/command-prompt-template.js +1 -1
  127. package/lib/common/user-interaction-tool.d.ts +19 -1
  128. package/lib/common/user-interaction-tool.d.ts.map +1 -1
  129. package/lib/common/user-interaction-tool.js +100 -29
  130. package/lib/common/user-interaction-tool.js.map +1 -1
  131. package/lib/common/user-interaction-tool.spec.js +215 -174
  132. package/lib/common/user-interaction-tool.spec.js.map +1 -1
  133. package/package.json +26 -25
  134. package/src/browser/ai-allow-all-mode-chat-banner.tsx +2 -2
  135. package/src/browser/ai-aware-scm-commit-widget.tsx +153 -0
  136. package/src/browser/ai-configuration/ai-configuration-detail-widget.spec.ts +14 -14
  137. package/src/browser/ai-configuration/ai-configuration-detail-widget.tsx +12 -7
  138. package/src/browser/ai-configuration/ai-configuration-tree-widget.tsx +8 -0
  139. package/src/browser/ai-configuration/categories/agent-capabilities-settings.tsx +7 -8
  140. package/src/browser/ai-configuration/categories/agent-chips.tsx +1 -1
  141. package/src/browser/ai-configuration/categories/model-aliases-configuration-category.spec.ts +40 -2
  142. package/src/browser/ai-configuration/categories/model-aliases-configuration-category.tsx +31 -10
  143. package/src/browser/ai-configuration/categories/models-configuration-category.spec.ts +47 -2
  144. package/src/browser/ai-configuration/categories/models-configuration-category.tsx +52 -5
  145. package/src/browser/ai-configuration/categories/provider-model-discovery-section.spec.tsx +451 -0
  146. package/src/browser/ai-configuration/categories/provider-model-discovery-section.tsx +341 -0
  147. package/src/browser/ai-configuration/language-model-renderer.tsx +11 -3
  148. package/src/browser/ai-getting-started-commands.ts +33 -0
  149. package/src/browser/ai-getting-started-contribution.ts +64 -0
  150. package/src/browser/ai-getting-started-walkthrough.spec.ts +146 -0
  151. package/src/browser/ai-getting-started-walkthrough.ts +249 -0
  152. package/src/browser/ai-walkthrough-context-keys.spec.ts +209 -0
  153. package/src/browser/ai-walkthrough-context-keys.ts +147 -0
  154. package/src/browser/chat-session-item.tsx +2 -2
  155. package/src/browser/commit-message-agent.spec.ts +97 -0
  156. package/src/browser/commit-message-agent.ts +113 -0
  157. package/src/browser/commit-message-command-contribution.ts +67 -0
  158. package/src/browser/commit-message-commands.ts +25 -0
  159. package/src/browser/commit-message-prompt-template.ts +65 -0
  160. package/src/browser/commit-message-runner.spec.ts +266 -0
  161. package/src/browser/commit-message-runner.ts +163 -0
  162. package/src/browser/frontend-module.ts +27 -0
  163. package/src/browser/git-changes-tool.spec.ts +200 -0
  164. package/src/browser/git-changes-tool.ts +239 -0
  165. package/src/browser/ide-chat-welcome-message-provider.tsx +93 -230
  166. package/src/browser/review/pr-review-capability-contribution.ts +1 -1
  167. package/src/browser/style/commit-message-overlay.css +64 -0
  168. package/src/browser/style/index.css +4 -42
  169. package/src/browser/user-interaction-tool-renderer.tsx +38 -8
  170. package/src/browser/user-interaction-tool.spec.ts +201 -2
  171. package/src/browser/user-interaction-tool.ts +69 -18
  172. package/src/common/command-prompt-template.ts +1 -1
  173. package/src/common/user-interaction-tool.spec.ts +217 -161
  174. 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
- languageModels?.slice().sort((a, b) => (a.name ?? a.id).localeCompare(b.name ?? b.id)).forEach(model => {
252
+ const modelOption = (model: LanguageModel): SelectOption => {
252
253
  const isNotReady = model.status.status !== 'ready';
253
- options.push({
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
+ });