@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.
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,451 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
18
+ let disableJSDOM = enableJSDOM();
19
+
20
+ // @lumino/dragdrop (pulled in transitively via `codicon` from `@theia/core/lib/browser`) extends the
21
+ // DragEvent DOM global at module load, which JSDOM does not provide; stub it so the import succeeds.
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ if (!(global as any).DragEvent) {
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ (global as any).DragEvent = class DragEvent extends (global as any).Event { };
26
+ }
27
+
28
+ import { expect } from 'chai';
29
+ import * as React from '@theia/core/shared/react';
30
+ import { flushSync } from '@theia/core/shared/react-dom';
31
+ import { createRoot } from '@theia/core/shared/react-dom/client';
32
+ import { LanguageModel } from '@theia/ai-core/lib/common';
33
+ import { DiscoveredModel, ModelDiscoveryAction, ModelDiscoveryStatus } from '@theia/ai-core/lib/common/model-discovery-status';
34
+ import { FavoriteModelsService } from '@theia/ai-core/lib/browser';
35
+ import { Emitter } from '@theia/core';
36
+ import { ProviderModelDiscoverySection } from './provider-model-discovery-section';
37
+
38
+ disableJSDOM();
39
+
40
+ function model(id: string, ready: boolean = true, released?: number): LanguageModel {
41
+ return { id, released, status: { status: ready ? 'ready' : 'unavailable', message: ready ? undefined : 'nope' } } as LanguageModel;
42
+ }
43
+
44
+ function status(overrides: Partial<ModelDiscoveryStatus> = {}): ModelDiscoveryStatus {
45
+ return { providerId: 'anthropic', label: 'Anthropic', state: 'ready', ...overrides };
46
+ }
47
+
48
+ describe('ProviderModelDiscoverySection', () => {
49
+
50
+ before(() => disableJSDOM = enableJSDOM());
51
+ after(() => disableJSDOM());
52
+
53
+ /** Stands in for the service: featured = the ids given, marked = the rest of `favorite`, hidden = taken out again. */
54
+ function favoritesStub(featured: string[] = [], marked: string[] = [], hidden: string[] = []): FavoriteModelsService {
55
+ const emitter = new Emitter<void>();
56
+ const toggled: string[] = [];
57
+ const reset: string[] = [];
58
+ const stub: Partial<FavoriteModelsService> & { toggled: string[]; reset: string[] } = {
59
+ toggled,
60
+ reset,
61
+ hasOverrides: () => marked.length > 0 || hidden.length > 0,
62
+ resetToDefaults: async (prefix: string) => { reset.push(prefix); },
63
+ onDidChange: emitter.event,
64
+ isFeatured: (id: string) => featured.includes(id),
65
+ isFavorite: (id: string) => !hidden.includes(id) && (featured.includes(id) || marked.includes(id)),
66
+ getFavorites: () => marked,
67
+ toggleFavorite: async (id: string) => { toggled.push(id); }
68
+ };
69
+ return stub as unknown as FavoriteModelsService;
70
+ }
71
+
72
+ function mount(
73
+ models: LanguageModel[],
74
+ discoveryStatus: ModelDiscoveryStatus = status(),
75
+ favorites: FavoriteModelsService = favoritesStub(),
76
+ actions: ModelDiscoveryAction[] = []
77
+ ): { container: HTMLElement; dispose: () => void } {
78
+ const container = document.createElement('div');
79
+ document.body.appendChild(container);
80
+ const root = createRoot(container);
81
+ flushSync(() => root.render(React.createElement(ProviderModelDiscoverySection, {
82
+ status: discoveryStatus,
83
+ models,
84
+ favorites,
85
+ onRefresh: () => { },
86
+ onAction: (action: ModelDiscoveryAction) => actions.push(action)
87
+ })));
88
+ return { container, dispose: () => { flushSync(() => root.unmount()); container.remove(); } };
89
+ }
90
+
91
+ /** The section header's own buttons, refresh first and reset after it. */
92
+ function sectionActions(container: HTMLElement): HTMLButtonElement[] {
93
+ return [...container.querySelectorAll<HTMLButtonElement>('.ai-configuration-section-title-actions .ai-configuration-icon-button')];
94
+ }
95
+
96
+ function labels(container: HTMLElement): string[] {
97
+ return [...container.querySelectorAll('.ai-configuration-item-row-label')].map(row => row.textContent ?? '');
98
+ }
99
+
100
+ function typeIntoFilter(container: HTMLElement, value: string): void {
101
+ const input = container.querySelector<HTMLInputElement>('.ai-configuration-filter-input input')!;
102
+ const view = container.ownerDocument.defaultView as unknown as { Event: typeof Event; HTMLInputElement: typeof HTMLInputElement };
103
+ // Assigning `value` directly goes through React's patched setter, which then treats the change as
104
+ // already handled and never calls `onChange`; the prototype's setter is what a real keystroke uses.
105
+ const nativeSetter = Object.getOwnPropertyDescriptor(view.HTMLInputElement.prototype, 'value')!.set!;
106
+ flushSync(() => {
107
+ nativeSetter.call(input, value);
108
+ input.dispatchEvent(new view.Event('input', { bubbles: true }));
109
+ });
110
+ }
111
+
112
+ it('lists the most recently released models first', () => {
113
+ const { container, dispose } = mount([
114
+ model('anthropic/claude-haiku-4-5', true, Date.parse('2025-10-01')),
115
+ model('anthropic/claude-opus-5', true, Date.parse('2026-04-01'))
116
+ ]);
117
+ try {
118
+ expect(labels(container)).to.deep.equal(['claude-opus-5', 'claude-haiku-4-5']);
119
+ } finally {
120
+ dispose();
121
+ }
122
+ });
123
+
124
+ it('falls back to alphabetical order for a provider that reports no release dates', () => {
125
+ const { container, dispose } = mount([model('google/gemini-3.7-flash'), model('google/gemini-3.1-pro')]);
126
+ try {
127
+ expect(labels(container)).to.deep.equal(['gemini-3.1-pro', 'gemini-3.7-flash']);
128
+ } finally {
129
+ dispose();
130
+ }
131
+ });
132
+
133
+ describe('what the chat input shows', () => {
134
+
135
+ /** Ten models, so the section also offers the filter box. */
136
+ function manyModels(): LanguageModel[] {
137
+ return ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'].map(name => model(`anthropic/claude-${name}`));
138
+ }
139
+
140
+ function badges(container: HTMLElement): string[][] {
141
+ return [...container.querySelectorAll('.ai-configuration-item-row')]
142
+ .map(row => [...row.querySelectorAll('.ai-configuration-kind-badge-label')].map(badge => badge.textContent ?? ''));
143
+ }
144
+
145
+ it('lifts the provider\'s current models to the top of the list', () => {
146
+ const favorites = favoritesStub(['anthropic/claude-c', 'anthropic/claude-h']);
147
+ const { container, dispose } = mount(manyModels(), status(), favorites);
148
+ try {
149
+ expect(labels(container).slice(0, 2)).to.deep.equal(['claude-c', 'claude-h']);
150
+ // The rest keeps the order it had.
151
+ expect(labels(container).slice(2, 4)).to.deep.equal(['claude-a', 'claude-b']);
152
+ } finally {
153
+ dispose();
154
+ }
155
+ });
156
+
157
+ it('badges them as the default, whether they are checked or not', () => {
158
+ // `claude-c` is one of the newest and unchecked all the same, `claude-h` is the user's own pick.
159
+ const favorites = favoritesStub(['anthropic/claude-c'], ['anthropic/claude-h'], ['anthropic/claude-c']);
160
+ const { container, dispose } = mount(manyModels(), status(), favorites);
161
+ try {
162
+ expect(labels(container)[0]).to.equal('claude-c');
163
+ expect(badges(container)[0]).to.deep.equal(['Default']);
164
+ // The badge says where a model stands, not whether it is shown: it survives unchecking.
165
+ const shown = container.querySelectorAll<HTMLButtonElement>('.ai-configuration-item-row-trailing .ai-configuration-icon-button')[0];
166
+ expect(shown.querySelector('.codicon-circle-large')).to.not.equal(null); // eslint-disable-line no-null/no-null
167
+ } finally {
168
+ dispose();
169
+ }
170
+ });
171
+
172
+ it('badges nothing on a model the provider does not count among its current ones', () => {
173
+ const favorites = favoritesStub([], ['anthropic/claude-h']);
174
+ const { container, dispose } = mount(manyModels(), status(), favorites);
175
+ try {
176
+ expect(badges(container).every(row => row.length === 0)).to.equal(true);
177
+ } finally {
178
+ dispose();
179
+ }
180
+ });
181
+
182
+ it('explains on the badge what a default is, and on the check only what it does', () => {
183
+ const favorites = favoritesStub(['anthropic/claude-c']);
184
+ const { container, dispose } = mount(manyModels(), status(), favorites);
185
+ try {
186
+ expect(container.querySelector('.ai-configuration-kind-badge')?.getAttribute('title'))
187
+ .to.contain('newest models of this provider');
188
+ const shown = container.querySelectorAll<HTMLButtonElement>('.ai-configuration-item-row-trailing .ai-configuration-icon-button')[0];
189
+ expect(shown.title).to.equal('Hide this model from the AI chat input\'s model picker');
190
+ } finally {
191
+ dispose();
192
+ }
193
+ });
194
+
195
+ it('offers a reset once the user decided something, and nothing to reset before', () => {
196
+ const untouched = favoritesStub(['anthropic/claude-c']);
197
+ const first = mount(manyModels(), status(), untouched);
198
+ try {
199
+ const reset = sectionActions(first.container)[1];
200
+ expect(reset.disabled).to.equal(true);
201
+ expect(reset.title).to.equal('Already at the defaults');
202
+ } finally {
203
+ first.dispose();
204
+ }
205
+
206
+ const touched = favoritesStub(['anthropic/claude-c'], ['anthropic/claude-h']);
207
+ const second = mount(manyModels(), status({ providerId: 'openAiOfficial', modelIdPrefix: 'openai' }), touched);
208
+ try {
209
+ const reset = sectionActions(second.container)[1];
210
+ expect(reset.disabled).to.equal(false);
211
+ flushSync(() => reset.click());
212
+ // Only this provider is reset, under the prefix its models are registered with.
213
+ expect((touched as unknown as { reset: string[] }).reset).to.deep.equal(['openai']);
214
+ } finally {
215
+ second.dispose();
216
+ }
217
+ });
218
+
219
+ it('reports how much of the list the chat input shows', () => {
220
+ const favorites = favoritesStub(['anthropic/claude-c'], ['anthropic/claude-h']);
221
+ const { container, dispose } = mount(manyModels(), status(), favorites);
222
+ try {
223
+ expect(container.querySelector('.ai-configuration-section-subtitle')?.textContent)
224
+ .to.equal('2 of 10 shown in the AI chat input\'s model picker');
225
+ } finally {
226
+ dispose();
227
+ }
228
+ });
229
+ });
230
+
231
+ it('heads every row with the id and puts the reported name beneath it', () => {
232
+ const discovered: DiscoveredModel[] = [{ id: 'claude-opus-5', label: 'Claude Opus 5' }];
233
+ const { container, dispose } = mount([model('anthropic/claude-opus-5')], status({ discovered }));
234
+ try {
235
+ expect(labels(container)).to.deep.equal(['claude-opus-5']);
236
+ expect(container.querySelector('.ai-configuration-item-row-description')?.textContent).to.equal('Claude Opus 5');
237
+ } finally {
238
+ dispose();
239
+ }
240
+ });
241
+
242
+ it('appends the reported description to the reported name', () => {
243
+ const discovered: DiscoveredModel[] = [{ id: 'gemini-3-pro', label: 'Gemini 3 Pro', description: 'The best one.' }];
244
+ const { container, dispose } = mount([model('google/gemini-3-pro')], status({ providerId: 'google', discovered }));
245
+ try {
246
+ expect(container.querySelector('.ai-configuration-item-row-description')?.textContent).to.equal('Gemini 3 Pro · The best one.');
247
+ } finally {
248
+ dispose();
249
+ }
250
+ });
251
+
252
+ it('keeps the same row shape for a provider that reports nothing but the id', () => {
253
+ const { container, dispose } = mount([model('openai/gpt-5.6-sol')], status({ providerId: 'openAiOfficial', modelIdPrefix: 'openai' }));
254
+ try {
255
+ const row = container.querySelector('.ai-configuration-item-row-label')!;
256
+ expect(row.textContent).to.equal('gpt-5.6-sol');
257
+ // Same heading as a provider that does report a name, just without the second line.
258
+ expect(row.className.split(/\s+/)).to.not.include('mono');
259
+ expect(container.querySelector('.ai-configuration-item-row-description')).to.equal(null); // eslint-disable-line no-null/no-null
260
+ } finally {
261
+ dispose();
262
+ }
263
+ });
264
+
265
+ it('badges only the models that are not ready, since a listed model is ready by construction', () => {
266
+ const { container, dispose } = mount([model('anthropic/claude-opus-5'), model('anthropic/claude-haiku-4-5', false)]);
267
+ try {
268
+ const badges = [...container.querySelectorAll('.ai-configuration-item-row .ai-configuration-status-badge-label')];
269
+ expect(badges.map(badge => badge.textContent)).to.deep.equal(['Not ready']);
270
+ } finally {
271
+ dispose();
272
+ }
273
+ });
274
+
275
+ it('offers no filter for a short list', () => {
276
+ const { container, dispose } = mount([model('anthropic/claude-opus-5')]);
277
+ try {
278
+ expect(container.querySelector('.ai-configuration-filter-input')).to.equal(null); // eslint-disable-line no-null/no-null
279
+ } finally {
280
+ dispose();
281
+ }
282
+ });
283
+
284
+ it('filters a long list by id', () => {
285
+ const models = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'].map(suffix => model(`openai/gpt-${suffix}`));
286
+ const { container, dispose } = mount(models);
287
+ try {
288
+ expect(labels(container)).to.have.lengthOf(9);
289
+ typeIntoFilter(container, 'gpt-c');
290
+ expect(labels(container)).to.deep.equal(['gpt-c']);
291
+ typeIntoFilter(container, 'nothing-matches');
292
+ expect(labels(container)).to.be.empty;
293
+ expect(container.querySelector('.ai-configuration-empty-state')).to.not.equal(null); // eslint-disable-line no-null/no-null
294
+ } finally {
295
+ dispose();
296
+ }
297
+ });
298
+
299
+ it('keeps the models listed when a refresh failed, above the failure callout', () => {
300
+ const { container, dispose } = mount([model('anthropic/claude-opus-5')], status({ state: 'error', message: 'offline' }));
301
+ try {
302
+ expect(container.querySelector('.ai-configuration-callout-text')?.textContent).to.equal('offline');
303
+ expect(labels(container)).to.deep.equal(['claude-opus-5']);
304
+ } finally {
305
+ dispose();
306
+ }
307
+ });
308
+
309
+ it('states that the list is configured manually when the models are overridden', () => {
310
+ const { container, dispose } = mount(
311
+ [model('anthropic/claude-opus-5')],
312
+ status({ state: 'overridden', message: 'The model list is configured manually.' })
313
+ );
314
+ try {
315
+ expect(container.querySelector('.ai-configuration-section-subtitle')?.textContent).to.equal('The model list is configured manually.');
316
+ expect(labels(container)).to.deep.equal(['claude-opus-5']);
317
+ const badges = [...container.querySelectorAll('.ai-configuration-status-badge-label')].map(badge => badge.textContent);
318
+ expect(badges).to.include('Manual');
319
+ } finally {
320
+ dispose();
321
+ }
322
+ });
323
+
324
+ it('names the release date of the models that report one, which is what the order follows', () => {
325
+ const { container, dispose } = mount([
326
+ model('anthropic/claude-opus-5', true, Date.parse('2026-04-01')),
327
+ model('anthropic/claude-undated')
328
+ ]);
329
+ try {
330
+ const details = [...container.querySelectorAll('.ai-configuration-item-row-detail')];
331
+ // Only the model with a reported date carries one; the other row simply has no trailing detail.
332
+ expect(details).to.have.lengthOf(1);
333
+ expect(details[0].textContent).to.match(/^released /);
334
+ } finally {
335
+ dispose();
336
+ }
337
+ });
338
+
339
+ it('checks a featured model and lets it be hidden again', () => {
340
+ const favorites = favoritesStub(['anthropic/claude-opus-5']);
341
+ const { container, dispose } = mount([model('anthropic/claude-opus-5')], status(), favorites);
342
+ try {
343
+ const shown = container.querySelector<HTMLButtonElement>('.ai-configuration-item-row-trailing .ai-configuration-icon-button')!;
344
+ expect(shown.querySelector('.codicon-pass')).to.not.equal(null); // eslint-disable-line no-null/no-null
345
+ expect(shown.disabled).to.equal(false);
346
+ // The check says what it does; what a default is belongs to the badge on the row.
347
+ expect(shown.title).to.equal('Hide this model from the AI chat input\'s model picker');
348
+ flushSync(() => shown.click());
349
+ expect((favorites as unknown as { toggled: string[] }).toggled).to.deep.equal(['anthropic/claude-opus-5']);
350
+ } finally {
351
+ dispose();
352
+ }
353
+ });
354
+
355
+ it('shows an empty circle on a model the chat input hides, and checks it on click', () => {
356
+ const favorites = favoritesStub();
357
+ const { container, dispose } = mount([model('anthropic/claude-opus-5-20260401')], status(), favorites);
358
+ try {
359
+ const shown = container.querySelector<HTMLButtonElement>('.ai-configuration-item-row-trailing .ai-configuration-icon-button')!;
360
+ expect(shown.querySelector('.codicon-circle-large')).to.not.equal(null); // eslint-disable-line no-null/no-null
361
+ expect(shown.title).to.contain('AI chat input');
362
+ expect(shown.disabled).to.equal(false);
363
+ flushSync(() => shown.click());
364
+ expect((favorites as unknown as { toggled: string[] }).toggled).to.deep.equal(['anthropic/claude-opus-5-20260401']);
365
+ } finally {
366
+ dispose();
367
+ }
368
+ });
369
+
370
+ it('states once, above the list, that this is about the chat input', () => {
371
+ const { container, dispose } = mount([model('anthropic/claude-opus-5')]);
372
+ try {
373
+ expect(container.querySelector('.ai-configuration-section-subtitle')?.textContent).to.contain('AI chat input');
374
+ } finally {
375
+ dispose();
376
+ }
377
+ });
378
+
379
+ it('filters the list down to the offered models on request', () => {
380
+ const models = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'].map(suffix => model(`openai/gpt-${suffix}`));
381
+ const { container, dispose } = mount(models, status({ providerId: 'openAiOfficial', modelIdPrefix: 'openai' }), favoritesStub(['openai/gpt-c']));
382
+ try {
383
+ expect(labels(container)).to.have.lengthOf(9);
384
+ // The narrowing sits in the filter box, next to the clear button.
385
+ const toggle = container.querySelector<HTMLButtonElement>('.ai-configuration-filter-input-action')!;
386
+ flushSync(() => toggle.click());
387
+ expect(labels(container)).to.deep.equal(['gpt-c']);
388
+ } finally {
389
+ dispose();
390
+ }
391
+ });
392
+
393
+ it('offers the command a provider names for its missing credential, e.g. signing in to Copilot', () => {
394
+ const actions: ModelDiscoveryAction[] = [];
395
+ const { container, dispose } = mount(
396
+ [],
397
+ status({
398
+ providerId: 'copilot',
399
+ state: 'no-credentials',
400
+ message: 'Not signed in to GitHub Copilot. Sign in to discover the models it offers.',
401
+ action: { label: 'Sign in to GitHub Copilot', commandId: 'copilot.signIn' }
402
+ }),
403
+ favoritesStub(),
404
+ actions
405
+ );
406
+ try {
407
+ const button = container.querySelector<HTMLButtonElement>('.ai-configuration-empty-state-action button')!;
408
+ expect(button.textContent).to.equal('Sign in to GitHub Copilot');
409
+ flushSync(() => button.click());
410
+ expect(actions.map(action => action.commandId)).to.deep.equal(['copilot.signIn']);
411
+ } finally {
412
+ dispose();
413
+ }
414
+ });
415
+
416
+ it('disables the refresh while there is no credential to fetch with, and says why', () => {
417
+ const { container, dispose } = mount([], status({ state: 'no-credentials', message: 'No Anthropic API key set.' }));
418
+ try {
419
+ const refresh = sectionActions(container)[0];
420
+ expect(refresh.disabled).to.equal(true);
421
+ expect(refresh.title).to.equal('No Anthropic API key set.');
422
+ } finally {
423
+ dispose();
424
+ }
425
+ });
426
+
427
+ it('lets a provider name the state itself, since a sign-in is not an API key', () => {
428
+ const { container, dispose } = mount([], status({
429
+ providerId: 'copilot',
430
+ state: 'no-credentials',
431
+ stateLabel: 'Not signed in',
432
+ message: 'Not signed in to GitHub Copilot. Sign in to discover the models it offers.'
433
+ }));
434
+ try {
435
+ const badge = container.querySelector('.ai-configuration-status-badge-label')!;
436
+ expect(badge.textContent).to.equal('Not signed in');
437
+ } finally {
438
+ dispose();
439
+ }
440
+ });
441
+
442
+ it('shows the key call to action instead of a list while there are no credentials', () => {
443
+ const { container, dispose } = mount([], status({ state: 'no-credentials', message: 'No Anthropic API key set.' }));
444
+ try {
445
+ expect(container.querySelector('.ai-configuration-empty-state-message')?.textContent).to.equal('No Anthropic API key set.');
446
+ expect(labels(container)).to.be.empty;
447
+ } finally {
448
+ dispose();
449
+ }
450
+ });
451
+ });