44 *--------------------------------------------------------------------------------------------*/
55
66import assert from 'assert' ;
7+ import { Emitter , Event } from '../../../../../../base/common/event.js' ;
78import { mock } from '../../../../../../base/test/common/mock.js' ;
89import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js' ;
10+ import { IAgentHostService } from '../../../../../../platform/agentHost/common/agentService.js' ;
911import type { IAgentSdkSetupInfo } from '../../../../../../platform/agentHost/common/agentSdkSetup.js' ;
10- import { AGENT_SDK_SETUP_DOWNLOAD_COMMAND_ID , AGENT_SDK_SETUP_GITHUB_SIGN_IN_COMMAND_ID , AGENT_SDK_SETUP_OPEN_DOCS_COMMAND_ID , AGENT_SDK_SETUP_RELOAD_COMMAND_ID , AGENT_SDK_SETUP_SIGN_IN_COMMAND_ID , agentSdkSetupNotificationId , createAgentSdkSetupNotification , getAgentDisplayNames , getAgentSdkSetupState , getAgentSdkSetupStateToReport , hasAgentSdkSetupNotification , type IAgentSdkSetupStateInputs } from '../../../browser/agentSessions/agentHost/agentHostSdkSetupNotification.js' ;
11- import type { AgentSdkSetupState } from '../../../../../services/agentHost/browser/agentSdkSetupService.js' ;
12- import { ChatInputNotificationActionKind , ChatInputNotificationSeverity , type IChatInputNotification , type IChatInputNotificationAction , type IChatInputNotificationService } from '../../../browser/widget/input/chatInputNotificationService.js' ;
12+ import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js' ;
13+ import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js' ;
14+ import { IDefaultAccountService } from '../../../../../../platform/defaultAccount/common/defaultAccount.js' ;
15+ import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js' ;
16+ import { AGENT_SDK_SETUP_DOWNLOAD_COMMAND_ID , AGENT_SDK_SETUP_GITHUB_SIGN_IN_COMMAND_ID , AGENT_SDK_SETUP_OPEN_DOCS_COMMAND_ID , AGENT_SDK_SETUP_RELOAD_COMMAND_ID , AGENT_SDK_SETUP_SIGN_IN_COMMAND_ID , AgentHostSdkSetupNotificationContribution , agentSdkSetupNotificationId , createAgentSdkSetupNotification , getAgentDisplayNames , getAgentSdkSetupState , getAgentSdkSetupStateToReport , hasAgentSdkSetupNotification , type IAgentSdkSetupStateInputs } from '../../../browser/agentSessions/agentHost/agentHostSdkSetupNotification.js' ;
17+ import { IAgentSdkSetupService , type AgentSdkSetupState } from '../../../../../services/agentHost/browser/agentSdkSetupService.js' ;
18+ import { ChatEntitlement , IChatEntitlementService } from '../../../../../services/chat/common/chatEntitlementService.js' ;
19+ import { ChatInputNotificationActionKind , ChatInputNotificationSeverity , IChatInputNotificationService , type IChatInputNotification , type IChatInputNotificationAction } from '../../../browser/widget/input/chatInputNotificationService.js' ;
1320import { SessionType } from '../../../common/chatSessionsService.js' ;
21+ import { ILanguageModelsService , type ILanguageModelChatMetadata } from '../../../common/languageModels.js' ;
1422
1523/** Signed out, flag on, entitlement settled, SDK missing — the case this feature exists for. */
1624const BLOCKED_USER : IAgentSdkSetupStateInputs = {
@@ -27,7 +35,7 @@ function commandIds(actions: readonly IChatInputNotificationAction[]): string[]
2735}
2836
2937suite ( 'Agent SDK setup banner' , ( ) => {
30- ensureNoDisposablesAreLeakedInTestSuite ( ) ;
38+ const store = ensureNoDisposablesAreLeakedInTestSuite ( ) ;
3139
3240 suite ( 'state' , ( ) => {
3341 const cases : readonly { readonly name : string ; readonly inputs : IAgentSdkSetupStateInputs ; readonly expected : AgentSdkSetupState | undefined } [ ] = [
@@ -78,6 +86,13 @@ suite('Agent SDK setup banner', () => {
7886 assert . deepStrictEqual ( notification . actions [ 0 ] . kind === ChatInputNotificationActionKind . Command ? notification . actions [ 0 ] . commandArgs : undefined , [ 'claude' ] ) ;
7987 } ) ;
8088
89+ test ( 'models add the send-a-message option without changing the Download action' , ( ) => {
90+ assert . deepStrictEqual ( createAgentSdkSetupNotification ( claude , 'Claude' , 'downloadOffered' , true ) , {
91+ ...createAgentSdkSetupNotification ( claude , 'Claude' , 'downloadOffered' ) ,
92+ description : 'Click Download or send a message to download the Claude Agent SDK.' ,
93+ } ) ;
94+ } ) ;
95+
8196 test ( 'every noun comes from the agent, so a second agent needs no entry here' , ( ) => {
8297 const codex : IAgentSdkSetupInfo = { agent : 'codex' , download : 'notDownloaded' , signInProviderName : 'ChatGPT' } ;
8398
@@ -195,6 +210,99 @@ suite('Agent SDK setup banner', () => {
195210 } ) ;
196211 } ) ;
197212
213+ suite ( 'model availability' , ( ) => {
214+ function createFixture ( initialSessionTypes : readonly ( string | undefined ) [ ] = [ ] ) {
215+ const instantiationService = store . add ( new TestInstantiationService ( ) ) ;
216+ const onDidChangeLanguageModels = store . add ( new Emitter < string > ( ) ) ;
217+ const models = new Map < string , ILanguageModelChatMetadata > ( ) ;
218+ const notifications : IChatInputNotification [ ] = [ ] ;
219+ const deletedNotifications : string [ ] = [ ] ;
220+ const reportedStates : AgentSdkSetupState [ ] = [ ] ;
221+ const setModels = ( sessionTypes : readonly ( string | undefined ) [ ] ) => {
222+ models . clear ( ) ;
223+ for ( const [ index , targetChatSessionType ] of sessionTypes . entries ( ) ) {
224+ models . set ( `model-${ index } ` , new class extends mock < ILanguageModelChatMetadata > ( ) {
225+ override readonly targetChatSessionType = targetChatSessionType ;
226+ } ( ) ) ;
227+ }
228+ onDidChangeLanguageModels . fire ( 'test' ) ;
229+ } ;
230+
231+ instantiationService . stub ( IChatInputNotificationService , {
232+ setNotification : notification => notifications . push ( notification ) ,
233+ deleteNotification : id => deletedNotifications . push ( id ) ,
234+ } ) ;
235+ instantiationService . stub ( IAgentSdkSetupService , {
236+ setups : [ { agent : 'claude' , download : 'notDownloaded' } ] ,
237+ onDidChangeSetups : Event . None ,
238+ isDownloadPending : ( ) => false ,
239+ reportSetupState : ( _agent , state ) => reportedStates . push ( state ) ,
240+ } ) ;
241+ instantiationService . stub ( IDefaultAccountService , {
242+ currentDefaultAccount : null ,
243+ onDidChangeDefaultAccount : Event . None ,
244+ } ) ;
245+ instantiationService . stub ( ILanguageModelsService , {
246+ onDidChangeLanguageModels : onDidChangeLanguageModels . event ,
247+ getLanguageModelIds : ( ) => [ ...models . keys ( ) ] ,
248+ lookupLanguageModel : id => models . get ( id ) ,
249+ } ) ;
250+ instantiationService . stub ( IConfigurationService , new TestConfigurationService ( ) ) ;
251+ instantiationService . stub ( IChatEntitlementService , {
252+ entitlement : ChatEntitlement . Pro ,
253+ onDidChangeEntitlement : Event . None ,
254+ } ) ;
255+ instantiationService . stub ( IAgentHostService , {
256+ onAgentHostStart : Event . None ,
257+ rootState : new class extends mock < IAgentHostService [ 'rootState' ] > ( ) {
258+ override readonly value = { agents : [ { provider : 'claude' , displayName : 'Claude' , description : '' , models : [ ] } ] } ;
259+ override readonly onDidChange = Event . None ;
260+ } ( ) ,
261+ } ) ;
262+
263+ setModels ( initialSessionTypes ) ;
264+ store . add ( instantiationService . createInstance ( AgentHostSdkSetupNotificationContribution ) ) ;
265+
266+ return { notifications, deletedNotifications, reportedStates, setModels } ;
267+ }
268+
269+ test ( 'explains download-on-use when models are already available' , ( ) => {
270+ const fixture = createFixture ( [ SessionType . AgentHostClaude ] ) ;
271+
272+ assert . deepStrictEqual ( fixture . notifications . map ( notification => notification . description ) , [
273+ 'Click Download or send a message to download the Claude Agent SDK.' ,
274+ ] ) ;
275+ } ) ;
276+
277+ test ( 'updates the visible offer when models for its agent appear and disappear' , ( ) => {
278+ const fixture = createFixture ( ) ;
279+ fixture . setModels ( [ SessionType . AgentHostCodex , undefined ] ) ;
280+ fixture . setModels ( [ SessionType . AgentHostCodex , undefined , SessionType . AgentHostClaude ] ) ;
281+ fixture . setModels ( [ SessionType . AgentHostClaude ] ) ;
282+ fixture . setModels ( [ ] ) ;
283+
284+ assert . deepStrictEqual ( {
285+ descriptions : fixture . notifications . map ( notification => notification . description ) ,
286+ actions : fixture . notifications . map ( notification => commandIds ( notification . actions ) ) ,
287+ deletedNotifications : fixture . deletedNotifications ,
288+ reportedStates : fixture . reportedStates ,
289+ } , {
290+ descriptions : [
291+ 'To use the Claude Agent, we need to download the Claude Agent SDK.' ,
292+ 'Click Download or send a message to download the Claude Agent SDK.' ,
293+ 'To use the Claude Agent, we need to download the Claude Agent SDK.' ,
294+ ] ,
295+ actions : [
296+ [ AGENT_SDK_SETUP_DOWNLOAD_COMMAND_ID ] ,
297+ [ AGENT_SDK_SETUP_DOWNLOAD_COMMAND_ID ] ,
298+ [ AGENT_SDK_SETUP_DOWNLOAD_COMMAND_ID ] ,
299+ ] ,
300+ deletedNotifications : [ ] ,
301+ reportedStates : [ 'downloadOffered' ] ,
302+ } ) ;
303+ } ) ;
304+ } ) ;
305+
198306 suite ( 'display names' , ( ) => {
199307 test ( 'reads each agent name the host published, and skips what it did not' , ( ) => {
200308 assert . deepStrictEqual ( [ ...getAgentDisplayNames ( {
0 commit comments