3
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
4
*--------------------------------------------------------------------------------------------*/
5
5
6
- import './media/mcp.css' ;
7
- import { reset } from '../../../../base/browser/dom.js' ;
6
+ import { addDisposableListener , EventType , h , reset } from '../../../../base/browser/dom.js' ;
8
7
import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js' ;
8
+ import { assertNever } from '../../../../base/common/assert.js' ;
9
9
import { Codicon } from '../../../../base/common/codicons.js' ;
10
10
import { diffSets , groupBy } from '../../../../base/common/collections.js' ;
11
11
import { Event } from '../../../../base/common/event.js' ;
12
- import { KeyMod , KeyCode } from '../../../../base/common/keyCodes.js' ;
12
+ import { KeyCode , KeyMod } from '../../../../base/common/keyCodes.js' ;
13
13
import { Disposable , DisposableStore } from '../../../../base/common/lifecycle.js' ;
14
14
import { autorun , derived , transaction } from '../../../../base/common/observable.js' ;
15
+ import { ThemeIcon } from '../../../../base/common/themables.js' ;
15
16
import { assertType } from '../../../../base/common/types.js' ;
16
17
import { ILocalizedString , localize , localize2 } from '../../../../nls.js' ;
17
18
import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js' ;
@@ -22,12 +23,14 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke
22
23
import { IInstantiationService , ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js' ;
23
24
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js' ;
24
25
import { IQuickInputService , IQuickPickItem , IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js' ;
26
+ import { spinningLoading } from '../../../../platform/theme/common/iconRegistry.js' ;
25
27
import { IWorkbenchContribution } from '../../../common/contributions.js' ;
26
28
import { CHAT_CATEGORY } from '../../chat/browser/actions/chatActions.js' ;
27
29
import { ChatAgentLocation } from '../../chat/common/chatAgents.js' ;
28
30
import { ChatContextKeys } from '../../chat/common/chatContextKeys.js' ;
29
31
import { McpContextKeys } from '../common/mcpContextKeys.js' ;
30
- import { IMcpServer , IMcpService , IMcpTool , McpConnectionState } from '../common/mcpTypes.js' ;
32
+ import { IMcpServer , IMcpService , IMcpTool , McpConnectionState , McpServerToolsState } from '../common/mcpTypes.js' ;
33
+ import './media/mcp.css' ;
31
34
32
35
// acroynms do not get localized
33
36
const category : ILocalizedString = {
@@ -65,7 +68,7 @@ export class ListMcpServerCommand extends Action2 {
65
68
...servers . map ( server => ( {
66
69
id : server . definition . id ,
67
70
label : server . definition . label ,
68
- description : McpConnectionState . toString ( server . state . read ( reader ) ) ,
71
+ description : McpConnectionState . toString ( server . connectionState . read ( reader ) ) ,
69
72
} ) ) ,
70
73
] ;
71
74
} ) ;
@@ -117,7 +120,7 @@ export class McpServerOptionsCommand extends Action2 {
117
120
}
118
121
119
122
const items : ActionItem [ ] = [ ] ;
120
- const serverState = server . state . get ( ) ;
123
+ const serverState = server . connectionState . get ( ) ;
121
124
122
125
// Only show start when server is stopped or in error state
123
126
if ( McpConnectionState . canBeStarted ( serverState . state ) ) {
@@ -230,14 +233,14 @@ export class AttachMCPToolsAction extends Action2 {
230
233
}
231
234
picks . push ( {
232
235
type : 'separator' ,
233
- label : localize ( 'desc' , "MCP Server - {0}" , McpConnectionState . toString ( server . state . get ( ) ) )
236
+ label : localize ( 'desc' , "MCP Server - {0}" , McpConnectionState . toString ( server . connectionState . get ( ) ) )
234
237
} ) ;
235
238
236
239
const item : ServerPick = {
237
240
server,
238
241
type : 'item' ,
239
242
label : `${ server . definition . label } ` ,
240
- description : localize ( 'desc' , "MCP Server - {0}" , McpConnectionState . toString ( server . state . get ( ) ) ) ,
243
+ description : localize ( 'desc' , "MCP Server - {0}" , McpConnectionState . toString ( server . connectionState . get ( ) ) ) ,
241
244
picked : tools . some ( tool => tool . enabled . get ( ) ) ,
242
245
toolPicks : [ ]
243
246
} ;
@@ -340,10 +343,17 @@ export class AttachMCPToolsActionRendering extends Disposable implements IWorkbe
340
343
constructor (
341
344
@IActionViewItemService actionViewItemService : IActionViewItemService ,
342
345
@IMcpService mcpService : IMcpService ,
343
- @IInstantiationService instaService : IInstantiationService
346
+ @IInstantiationService instaService : IInstantiationService ,
347
+ @ICommandService commandService : ICommandService ,
344
348
) {
345
349
super ( ) ;
346
350
351
+ const enum DisplayedState {
352
+ None ,
353
+ NewTools ,
354
+ Error ,
355
+ Refreshing ,
356
+ }
347
357
348
358
const toolsCount = derived ( r => {
349
359
let count = 0 ;
@@ -358,6 +368,30 @@ export class AttachMCPToolsActionRendering extends Disposable implements IWorkbe
358
368
return { count, enabled } ;
359
369
} ) ;
360
370
371
+ const displayedState = derived ( reader => {
372
+ const servers = mcpService . servers . read ( reader ) ;
373
+ const serversPerState : IMcpServer [ ] [ ] = [ ] ;
374
+ for ( const server of servers ) {
375
+ let thisState = DisplayedState . None ;
376
+ switch ( server . toolsState . read ( reader ) ) {
377
+ case McpServerToolsState . Unknown :
378
+ thisState = server . connectionState . read ( reader ) . state === McpConnectionState . Kind . Error ? DisplayedState . Error : DisplayedState . NewTools ;
379
+ break ;
380
+ case McpServerToolsState . RefreshingFromUnknown :
381
+ thisState = DisplayedState . Refreshing ;
382
+ break ;
383
+ case McpServerToolsState . Cached :
384
+ thisState = server . connectionState . read ( reader ) . state === McpConnectionState . Kind . Error ? DisplayedState . Error : DisplayedState . None ;
385
+ break ;
386
+ }
387
+
388
+ serversPerState [ thisState ] ??= [ ] ;
389
+ serversPerState [ thisState ] . push ( server ) ;
390
+ }
391
+
392
+ const maxState = ( serversPerState . length - 1 ) as DisplayedState ;
393
+ return { state : maxState , servers : serversPerState [ maxState ] } ;
394
+ } ) ;
361
395
362
396
this . _store . add ( actionViewItemService . register ( MenuId . ChatInputAttachmentToolbar , AttachMCPToolsAction . id , ( action , options ) => {
363
397
if ( ! ( action instanceof MenuItemAction ) ) {
@@ -371,6 +405,73 @@ export class AttachMCPToolsActionRendering extends Disposable implements IWorkbe
371
405
this . options . label = true ;
372
406
container . classList . add ( 'chat-mcp' ) ;
373
407
super . render ( container ) ;
408
+
409
+ const action = h ( 'button.chat-mcp-action' , [ h ( 'span@icon' ) ] ) ;
410
+
411
+ this . _register ( autorun ( r => {
412
+ const { state, servers } = displayedState . read ( r ) ;
413
+ const { root, icon } = action ;
414
+ this . updateTooltip ( ) ;
415
+ container . classList . toggle ( 'chat-mcp-has-action' , state !== DisplayedState . None ) ;
416
+
417
+ if ( state === DisplayedState . None ) {
418
+ root . remove ( ) ;
419
+ return ;
420
+ }
421
+
422
+ if ( ! root . parentElement ) {
423
+ container . appendChild ( root ) ;
424
+ }
425
+
426
+ root . ariaLabel = this . getLabelForState ( { state, servers } ) ;
427
+ root . className = 'chat-mcp-action' ;
428
+ icon . className = '' ;
429
+ if ( state === DisplayedState . NewTools ) {
430
+ root . classList . add ( 'chat-mcp-action-new' ) ;
431
+ icon . classList . add ( ...ThemeIcon . asClassNameArray ( Codicon . refresh ) ) ;
432
+ } else if ( state === DisplayedState . Error ) {
433
+ root . classList . add ( 'chat-mcp-action-error' ) ;
434
+ icon . classList . add ( ...ThemeIcon . asClassNameArray ( Codicon . warning ) ) ;
435
+ } else if ( state === DisplayedState . Refreshing ) {
436
+ root . classList . add ( 'chat-mcp-action-refreshing' ) ;
437
+ icon . classList . add ( ...ThemeIcon . asClassNameArray ( spinningLoading ) ) ;
438
+ } else {
439
+ assertNever ( state ) ;
440
+ }
441
+ } ) ) ;
442
+
443
+ this . _register ( addDisposableListener ( action . root , EventType . CLICK , e => {
444
+ e . preventDefault ( ) ;
445
+ e . stopPropagation ( ) ;
446
+
447
+ const { state, servers } = displayedState . get ( ) ;
448
+ if ( state === DisplayedState . NewTools ) {
449
+ servers . forEach ( server => server . start ( ) ) ;
450
+ } else if ( state === DisplayedState . Refreshing ) {
451
+ servers . at ( - 1 ) ?. showOutput ( ) ;
452
+ } else if ( state === DisplayedState . Error ) {
453
+ const server = servers . at ( - 1 ) ;
454
+ if ( server ) {
455
+ commandService . executeCommand ( McpServerOptionsCommand . id , server . definition . id ) ;
456
+ }
457
+ }
458
+ } ) ) ;
459
+ }
460
+
461
+ protected override getTooltip ( ) : string {
462
+ return this . getLabelForState ( ) || super . getTooltip ( ) ;
463
+ }
464
+
465
+ private getLabelForState ( { state, servers } = displayedState . get ( ) ) {
466
+ if ( state === DisplayedState . NewTools ) {
467
+ return localize ( 'mcp.newTools' , "New tools available ({0})" , servers . length ) ;
468
+ } else if ( state === DisplayedState . Error ) {
469
+ return localize ( 'mcp.toolError' , "Error loading {0} tool(s)" , servers . length ) ;
470
+ } else if ( state === DisplayedState . Refreshing ) {
471
+ return localize ( 'mcp.toolRefresh' , "Discovering tools..." ) ;
472
+ } else {
473
+ return null ;
474
+ }
374
475
}
375
476
376
477
protected override updateLabel ( ) : void {
0 commit comments