3
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
4
*--------------------------------------------------------------------------------------------*/
5
5
6
- import { h } from '../../../../base/browser/dom.js' ;
6
+ import { $ , addDisposableListener , disposableWindowInterval , EventType , h } from '../../../../base/browser/dom.js' ;
7
+ import { renderMarkdown } from '../../../../base/browser/markdownRenderer.js' ;
8
+ import { IManagedHoverTooltipHTMLElement } from '../../../../base/browser/ui/hover/hover.js' ;
9
+ import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js' ;
10
+ import { mainWindow } from '../../../../base/browser/window.js' ;
11
+ import { findLast } from '../../../../base/common/arraysFind.js' ;
7
12
import { assertNever } from '../../../../base/common/assert.js' ;
8
13
import { VSBuffer } from '../../../../base/common/buffer.js' ;
9
14
import { Codicon } from '../../../../base/common/codicons.js' ;
10
15
import { groupBy } from '../../../../base/common/collections.js' ;
11
16
import { Event } from '../../../../base/common/event.js' ;
12
- import { Disposable , DisposableStore } from '../../../../base/common/lifecycle.js' ;
13
- import { autorun , derived } from '../../../../base/common/observable.js' ;
17
+ import { markdownCommandLink , MarkdownString } from '../../../../base/common/htmlContent.js' ;
18
+ import { Disposable , DisposableStore , toDisposable } from '../../../../base/common/lifecycle.js' ;
19
+ import { autorun , derived , derivedObservableWithCache , observableValue } from '../../../../base/common/observable.js' ;
14
20
import { ThemeIcon } from '../../../../base/common/themables.js' ;
15
21
import { isDefined } from '../../../../base/common/types.js' ;
16
22
import { URI } from '../../../../base/common/uri.js' ;
@@ -31,6 +37,7 @@ import { IOpenerService } from '../../../../platform/opener/common/opener.js';
31
37
import { IProductService } from '../../../../platform/product/common/productService.js' ;
32
38
import { IQuickInputService , IQuickPickItem , IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js' ;
33
39
import { StorageScope } from '../../../../platform/storage/common/storage.js' ;
40
+ import { defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js' ;
34
41
import { spinningLoading } from '../../../../platform/theme/common/iconRegistry.js' ;
35
42
import { IWorkspaceContextService , IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js' ;
36
43
import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from '../../../browser/actions/workspaceCommands.js' ;
@@ -54,9 +61,10 @@ import { TEXT_FILE_EDITOR_ID } from '../../files/common/files.js';
54
61
import { McpCommandIds } from '../common/mcpCommandIds.js' ;
55
62
import { McpContextKeys } from '../common/mcpContextKeys.js' ;
56
63
import { IMcpRegistry } from '../common/mcpRegistryTypes.js' ;
57
- import { HasInstalledMcpServersContext , IMcpSamplingService , IMcpServer , IMcpServerStartOpts , IMcpService , InstalledMcpServersViewId , LazyCollectionState , McpCapability , McpConnectionState , McpDefinitionReference , mcpPromptPrefix , McpServerCacheState , McpStartServerInteraction } from '../common/mcpTypes.js' ;
64
+ import { HasInstalledMcpServersContext , IMcpSamplingService , IMcpServer , IMcpServerStartOpts , IMcpService , InstalledMcpServersViewId , LazyCollectionState , McpCapability , McpCollectionDefinition , McpConnectionState , McpDefinitionReference , mcpPromptPrefix , McpServerCacheState , McpStartServerInteraction } from '../common/mcpTypes.js' ;
58
65
import { McpAddConfigurationCommand } from './mcpCommandsAddConfiguration.js' ;
59
66
import { McpResourceQuickAccess , McpResourceQuickPick } from './mcpResourceQuickAccess.js' ;
67
+ import './media/mcpServerAction.css' ;
60
68
import { openPanelChatAndGetWidget } from './openPanelChatAndGetWidget.js' ;
61
69
62
70
// acroynms do not get localized
@@ -358,6 +366,7 @@ export class MCPServerActionRendering extends Disposable implements IWorkbenchCo
358
366
) {
359
367
super ( ) ;
360
368
369
+ const hoverIsOpen = observableValue ( this , false ) ;
361
370
const config = observableConfigValue ( mcpAutoStartConfig , McpAutoStartValue . NewAndOutdated , configurationService ) ;
362
371
363
372
const enum DisplayedState {
@@ -367,9 +376,18 @@ export class MCPServerActionRendering extends Disposable implements IWorkbenchCo
367
376
Refreshing ,
368
377
}
369
378
370
- const displayedState = derived ( ( reader ) => {
379
+ type DisplayedStateT = {
380
+ state : DisplayedState ;
381
+ servers : ( IMcpServer | McpCollectionDefinition ) [ ] ;
382
+ } ;
383
+
384
+ function isServer ( s : IMcpServer | McpCollectionDefinition ) : s is IMcpServer {
385
+ return typeof ( s as IMcpServer ) . start === 'function' ;
386
+ }
387
+
388
+ const displayedStateCurrent = derived ( ( reader ) : DisplayedStateT => {
371
389
const servers = mcpService . servers . read ( reader ) ;
372
- const serversPerState : IMcpServer [ ] [ ] = [ ] ;
390
+ const serversPerState : ( IMcpServer | McpCollectionDefinition ) [ ] [ ] = [ ] ;
373
391
for ( const server of servers ) {
374
392
let thisState = DisplayedState . None ;
375
393
switch ( server . cacheState . read ( reader ) ) {
@@ -390,10 +408,12 @@ export class MCPServerActionRendering extends Disposable implements IWorkbenchCo
390
408
}
391
409
392
410
const unknownServerStates = mcpService . lazyCollectionState . read ( reader ) ;
393
- if ( unknownServerStates === LazyCollectionState . LoadingUnknown ) {
411
+ if ( unknownServerStates . state === LazyCollectionState . LoadingUnknown ) {
394
412
serversPerState [ DisplayedState . Refreshing ] ??= [ ] ;
395
- } else if ( unknownServerStates === LazyCollectionState . HasUnknown ) {
413
+ serversPerState [ DisplayedState . Refreshing ] . push ( ...unknownServerStates . collections ) ;
414
+ } else if ( unknownServerStates . state === LazyCollectionState . HasUnknown ) {
396
415
serversPerState [ DisplayedState . NewTools ] ??= [ ] ;
416
+ serversPerState [ DisplayedState . NewTools ] . push ( ...unknownServerStates . collections ) ;
397
417
}
398
418
399
419
let maxState = ( serversPerState . length - 1 ) as DisplayedState ;
@@ -404,6 +424,15 @@ export class MCPServerActionRendering extends Disposable implements IWorkbenchCo
404
424
return { state : maxState , servers : serversPerState [ maxState ] || [ ] } ;
405
425
} ) ;
406
426
427
+ // avoid hiding the hover if a state changes while it's open:
428
+ const displayedState = derivedObservableWithCache < DisplayedStateT > ( this , ( reader , last ) => {
429
+ if ( last && hoverIsOpen . read ( reader ) ) {
430
+ return last ;
431
+ } else {
432
+ return displayedStateCurrent . read ( reader ) ;
433
+ }
434
+ } ) ;
435
+
407
436
this . _store . add ( actionViewItemService . register ( MenuId . ChatExecute , McpCommandIds . ListServer , ( action , options ) => {
408
437
if ( ! ( action instanceof MenuItemAction ) ) {
409
438
return undefined ;
@@ -419,7 +448,8 @@ export class MCPServerActionRendering extends Disposable implements IWorkbenchCo
419
448
const action = h ( 'button.chat-mcp-action' , [ h ( 'span@icon' ) ] ) ;
420
449
421
450
this . _register ( autorun ( r => {
422
- const { state } = displayedState . read ( r ) ;
451
+ const displayed = displayedState . read ( r ) ;
452
+ const { state } = displayed ;
423
453
const { root, icon } = action ;
424
454
this . updateTooltip ( ) ;
425
455
container . classList . toggle ( 'chat-mcp-has-action' , state !== DisplayedState . None ) ;
@@ -428,7 +458,7 @@ export class MCPServerActionRendering extends Disposable implements IWorkbenchCo
428
458
container . appendChild ( root ) ;
429
459
}
430
460
431
- root . ariaLabel = this . getLabelForState ( displayedState . read ( r ) ) ;
461
+ root . ariaLabel = this . getLabelForState ( displayed ) ;
432
462
root . className = 'chat-mcp-action' ;
433
463
icon . className = '' ;
434
464
if ( state === DisplayedState . NewTools ) {
@@ -450,16 +480,17 @@ export class MCPServerActionRendering extends Disposable implements IWorkbenchCo
450
480
e . preventDefault ( ) ;
451
481
e . stopPropagation ( ) ;
452
482
453
- const { state, servers } = displayedState . get ( ) ;
483
+ const { state, servers } = displayedStateCurrent . get ( ) ;
454
484
if ( state === DisplayedState . NewTools ) {
455
485
const interaction = new McpStartServerInteraction ( ) ;
456
- servers . forEach ( server => server . stop ( ) . then ( ( ) => server . start ( { interaction } ) ) ) ;
486
+ servers . filter ( isServer ) . forEach ( server => server . stop ( ) . then ( ( ) => server . start ( { interaction } ) ) ) ;
457
487
mcpService . activateCollections ( ) ;
458
488
} else if ( state === DisplayedState . Refreshing ) {
459
- servers . at ( - 1 ) ?. showOutput ( ) ;
489
+ findLast ( servers , isServer ) ?. showOutput ( ) ;
460
490
} else if ( state === DisplayedState . Error ) {
461
- const server = servers . at ( - 1 ) ;
491
+ const server = findLast ( servers , isServer ) ;
462
492
if ( server ) {
493
+ server . showOutput ( ) ;
463
494
commandService . executeCommand ( McpCommandIds . ServerOptions , server . definition . id ) ;
464
495
}
465
496
} else {
@@ -471,7 +502,93 @@ export class MCPServerActionRendering extends Disposable implements IWorkbenchCo
471
502
return this . getLabelForState ( ) || super . getTooltip ( ) ;
472
503
}
473
504
474
- private getLabelForState ( { state, servers } = displayedState . get ( ) ) {
505
+ protected override getHoverContents ( { state, servers } = displayedStateCurrent . get ( ) ) : string | undefined | IManagedHoverTooltipHTMLElement {
506
+ const link = ( s : IMcpServer ) => markdownCommandLink ( {
507
+ title : s . definition . label ,
508
+ id : McpCommandIds . ServerOptions ,
509
+ arguments : [ s . definition . id ] ,
510
+ } ) ;
511
+
512
+ const single = servers . length === 1 ;
513
+ const names = servers . map ( s => isServer ( s ) ? link ( s ) : '`' + s . label + '`' ) . map ( l => single ? l : `- ${ l } \n` ) . join ( ', ' ) ;
514
+ let markdown : MarkdownString ;
515
+ if ( state === DisplayedState . NewTools ) {
516
+ markdown = new MarkdownString ( single
517
+ ? localize ( 'mcp.newTools.md.single' , "MCP server {0} has been updated and may have new tools available." , names )
518
+ : localize ( 'mcp.newTools.md.multi' , "MCP servers have been updated and may have new tools available:\n\n{0}" , names )
519
+ ) ;
520
+ } else if ( state === DisplayedState . Error ) {
521
+ markdown = new MarkdownString ( single
522
+ ? localize ( 'mcp.err.md.single' , "MCP server {0} was unable to start successfully." , names )
523
+ : localize ( 'mcp.err.md.multi' , "Multiple MCP servers were unable to start successfully:\n\n{0}" , names )
524
+ ) ;
525
+ } else {
526
+ return this . getLabelForState ( ) || undefined ;
527
+ }
528
+
529
+ return {
530
+ element : ( token ) : HTMLElement => {
531
+ hoverIsOpen . set ( true , undefined ) ;
532
+
533
+ const store = new DisposableStore ( ) ;
534
+ store . add ( toDisposable ( ( ) => hoverIsOpen . set ( false , undefined ) ) ) ;
535
+ store . add ( token . onCancellationRequested ( ( ) => {
536
+ store . dispose ( ) ;
537
+ } ) ) ;
538
+
539
+ // todo@connor 4312/@benibenj: workaround for #257923
540
+ store . add ( disposableWindowInterval ( mainWindow , ( ) => {
541
+ if ( ! container . isConnected ) {
542
+ store . dispose ( ) ;
543
+ }
544
+ } , 2000 ) ) ;
545
+
546
+ const container = $ ( 'div.mcp-hover-contents' ) ;
547
+
548
+ // Render markdown content
549
+ markdown . isTrusted = true ;
550
+ const markdownResult = store . add ( renderMarkdown ( markdown ) ) ;
551
+ container . appendChild ( markdownResult . element ) ;
552
+
553
+ // Add divider
554
+ const divider = $ ( 'hr.mcp-hover-divider' ) ;
555
+ container . appendChild ( divider ) ;
556
+
557
+ // Add checkbox for mcpAutoStartConfig setting
558
+ const checkboxContainer = $ ( 'div.mcp-hover-setting' ) ;
559
+ const settingLabelStr = localize ( 'mcp.autoStart' , "Automatically start MCP servers when sending a chat message" ) ;
560
+
561
+ const checkbox = store . add ( new Checkbox (
562
+ settingLabelStr ,
563
+ config . get ( ) !== McpAutoStartValue . Never ,
564
+ defaultCheckboxStyles
565
+ ) ) ;
566
+
567
+ checkboxContainer . appendChild ( checkbox . domNode ) ;
568
+
569
+ // Add label next to checkbox
570
+ const settingLabel = $ ( 'span.mcp-hover-setting-label' , undefined , settingLabelStr ) ;
571
+ checkboxContainer . appendChild ( settingLabel ) ;
572
+
573
+ const onChange = ( ) => {
574
+ const newValue = checkbox . checked ? McpAutoStartValue . NewAndOutdated : McpAutoStartValue . Never ;
575
+ configurationService . updateValue ( mcpAutoStartConfig , newValue ) ;
576
+ } ;
577
+
578
+ store . add ( checkbox . onChange ( onChange ) ) ;
579
+
580
+ store . add ( addDisposableListener ( settingLabel , EventType . CLICK , ( ) => {
581
+ checkbox . checked = ! checkbox . checked ;
582
+ onChange ( ) ;
583
+ } ) ) ;
584
+ container . appendChild ( checkboxContainer ) ;
585
+
586
+ return container ;
587
+ } ,
588
+ } ;
589
+ }
590
+
591
+ private getLabelForState ( { state, servers } = displayedStateCurrent . get ( ) ) {
475
592
if ( state === DisplayedState . NewTools ) {
476
593
return localize ( 'mcp.newTools' , "New tools available ({0})" , servers . length || 1 ) ;
477
594
} else if ( state === DisplayedState . Error ) {
@@ -482,8 +599,6 @@ export class MCPServerActionRendering extends Disposable implements IWorkbenchCo
482
599
return null ;
483
600
}
484
601
}
485
-
486
-
487
602
} , action , { ...options , keybindingNotRenderedWithLabel : true } ) ;
488
603
489
604
} , Event . fromObservable ( displayedState ) ) ) ;
0 commit comments