@@ -35,7 +35,6 @@ import type {
3535
3636import { ActivitySpinner } from '../components/chrome/activity-spinner' ;
3737import { buildGoalMarker } from '../components/messages/goal-markers' ;
38- import { StatusMessageComponent } from '../components/messages/status-message' ;
3938import {
4039 DynamicWorkflowModeMarkerComponent ,
4140 type DynamicWorkflowModeMarkerState ,
@@ -69,7 +68,7 @@ import {
6968 selectMcpStartupStatusRows ,
7069} from '../utils/mcp-server-status' ;
7170import { openUrl } from '#/utils/open-url' ;
72- import { currentTheme } from '#/tui/theme ' ;
71+ import { McpLoadingLine } from '../components/messages/mcp-loading-line ' ;
7372import type { ColorToken } from '#/tui/theme' ;
7473import { errorReportHintLine } from '../constant/feedback' ;
7574import { formatStepDebugTiming } from '#/utils/usage/debug-timing' ;
@@ -165,7 +164,7 @@ export class SessionEventHandler {
165164 renderedSkillActivationIds : Set < string > = new Set ( ) ;
166165 renderedPluginCommandActivationIds : Set < string > = new Set ( ) ;
167166 renderedMcpServerStatusKeys : Map < string , string > = new Map ( ) ;
168- mcpServerStatusSpinners : Map < string , ActivitySpinner > = new Map ( ) ;
167+ mcpLoadingLine : McpLoadingLine | undefined ;
169168 mcpServers : Map < string , McpServerStatusSnapshot > = new Map ( ) ;
170169 private goalCompletionAwaitingClear = false ;
171170 private goalCompletionTurnEnded = false ;
@@ -252,9 +251,10 @@ export class SessionEventHandler {
252251 this . renderMcpServerStatus ( server ) ;
253252 }
254253
254+ const live = new Map ( this . mcpServers ) ;
255255 this . mcpServers . clear ( ) ;
256256 for ( const server of servers ) {
257- this . mcpServers . set ( server . name , server ) ;
257+ this . mcpServers . set ( server . name , live . get ( server . name ) ?? server ) ;
258258 }
259259 const hidden : McpServerStatusSnapshot [ ] = [ ] ;
260260 for ( const server of servers ) {
@@ -263,8 +263,9 @@ export class SessionEventHandler {
263263 this . renderedMcpServerStatusKeys . set ( server . name , mcpServerStatusKey ( server ) ) ;
264264 hidden . push ( server ) ;
265265 }
266- const summary = formatMcpStartupStatusSummary ( servers ) ;
266+ const summary = formatMcpStartupStatusSummary ( [ ... this . mcpServers . values ( ) ] ) ;
267267 host . setAppState ( { mcpServersSummary : summary || null } ) ;
268+ this . updateMcpLoadingRow ( ) ;
268269 }
269270
270271 handleEvent ( event : Event , sendQueued : ( item : QueuedMessage ) => void ) : void {
@@ -321,10 +322,7 @@ export class SessionEventHandler {
321322 }
322323
323324 stopAllMcpServerStatusSpinners ( ) : void {
324- for ( const spinner of this . mcpServerStatusSpinners . values ( ) ) {
325- spinner . stop ( ) ;
326- }
327- this . mcpServerStatusSpinners . clear ( ) ;
325+ this . removeMcpLoadingRow ( ) ;
328326 }
329327
330328 // ---------------------------------------------------------------------------
@@ -1018,77 +1016,52 @@ export class SessionEventHandler {
10181016 this . host . setAppState ( { mcpServersSummary : summary || null } ) ;
10191017
10201018 switch ( server . status ) {
1021- case 'connected' : {
1022- const toolStr = `${ server . toolCount } tool${ server . toolCount === 1 ? '' : 's' } ` ;
1023- const message = `MCP server "${ server . name } " connected · ${ toolStr } (${ server . transport } )` ;
1024- this . finalizeMcpServerStatusRow ( server . name , message , 'success' ) ;
1025- return ;
1026- }
1027- case 'failed' : {
1028- const message = `MCP server "${ server . name } " failed${ server . error !== undefined ? `: ${ server . error } ` : '' } ` ;
1029- this . finalizeMcpServerStatusRow ( server . name , message , 'error' ) ;
1030- return ;
1031- }
1032- case 'needs-auth' : {
1033- const message = `MCP server "${ server . name } " needs OAuth — run /mcp-config login ${ server . name } ` ;
1034- this . finalizeMcpServerStatusRow ( server . name , message , 'warning' ) ;
1035- return ;
1036- }
1037- case 'disabled' :
1038- this . finalizeMcpServerStatusRow (
1039- server . name ,
1040- `MCP server "${ server . name } " disabled` ,
1041- 'textMuted' ,
1019+ case 'failed' :
1020+ this . host . showStatus (
1021+ `MCP server "${ server . name } " failed${ server . error !== undefined ? `: ${ server . error } ` : '' } ` ,
1022+ 'error' ,
10421023 ) ;
1043- return ;
1044- case 'removed' :
1045- this . finalizeMcpServerStatusRow (
1046- server . name ,
1047- `MCP server "${ server . name } " removed` ,
1048- 'textMuted' ,
1024+ break ;
1025+ case 'needs-auth' :
1026+ this . host . showStatus (
1027+ `MCP server "${ server . name } " needs OAuth — run /mcp-config login ${ server . name } ` ,
1028+ 'warning' ,
10491029 ) ;
1050- return ;
1051- case 'pending' :
1052- this . showMcpServerStatusSpinner ( server . name ) ;
1053- return ;
1030+ break ;
1031+ default :
1032+ break ;
10541033 }
1034+ this . updateMcpLoadingRow ( ) ;
10551035 }
10561036
1057- private showMcpServerStatusSpinner ( name : string ) : void {
1037+ // One transient row while servers connect; it leaves the transcript once none
1038+ // is pending. Connected servers are summarised in the welcome card instead.
1039+ private updateMcpLoadingRow ( ) : void {
10581040 const { state } = this . host ;
1059- const label = `MCP server "${ name } " connecting…` ;
1060- const existing = this . mcpServerStatusSpinners . get ( name ) ;
1061- if ( existing !== undefined ) {
1062- existing . setLabel ( label ) ;
1041+ const pending = [ ...this . mcpServers . values ( ) ]
1042+ . filter ( ( server ) => server . status === 'pending' )
1043+ . map ( ( server ) => server . name ) ;
1044+ if ( pending . length === 0 ) {
1045+ this . removeMcpLoadingRow ( ) ;
1046+ return ;
1047+ }
1048+ if ( this . mcpLoadingLine !== undefined ) {
1049+ this . mcpLoadingLine . setNames ( pending ) ;
10631050 return ;
10641051 }
1065- const tint = ( s : string ) : string => currentTheme . fg ( 'textMuted' , s ) ;
1066- const spinner = new ActivitySpinner ( state . ui , tint , label ) ;
1067- state . transcriptContainer . addChild ( spinner ) ;
1068- this . mcpServerStatusSpinners . set ( name , spinner ) ;
1052+ this . mcpLoadingLine = new McpLoadingLine ( state . ui , pending ) ;
1053+ state . transcriptContainer . addChild ( this . mcpLoadingLine ) ;
10691054 state . ui . requestRender ( ) ;
10701055 }
10711056
1072- private finalizeMcpServerStatusRow ( name : string , message : string , color : ColorToken ) : void {
1073- const { state } = this . host ;
1074- const spinner = this . mcpServerStatusSpinners . get ( name ) ;
1075- if ( spinner === undefined ) {
1076- this . host . showStatus ( message , color ) ;
1077- return ;
1078- }
1057+ private removeMcpLoadingRow ( ) : void {
1058+ const spinner = this . mcpLoadingLine ;
1059+ if ( spinner === undefined ) return ;
10791060 spinner . stop ( ) ;
1080- const status = new StatusMessageComponent ( message , color ) ;
1081- const children = state . transcriptContainer . children ;
1082- const idx = children . indexOf ( spinner ) ;
1083- if ( idx >= 0 ) {
1084- // In-place replacement is picked up by the container's ref-checked
1085- // render cache; a tree-wide invalidate is unnecessary (and costly).
1086- children [ idx ] = status ;
1087- } else {
1088- state . transcriptContainer . addChild ( status ) ;
1089- }
1090- this . mcpServerStatusSpinners . delete ( name ) ;
1091- state . ui . requestRender ( ) ;
1061+ // oxlint-disable-next-line unicorn/prefer-dom-node-remove
1062+ this . host . state . transcriptContainer . removeChild ( spinner ) ;
1063+ this . mcpLoadingLine = undefined ;
1064+ this . host . state . ui . requestRender ( ) ;
10921065 }
10931066
10941067 private handleSkillActivated ( event : SkillActivatedEvent ) : void {
0 commit comments