update loading?

This commit is contained in:
Andrew Pareles 2025-05-21 22:51:55 -07:00
parent 446248aa79
commit 57bc32ac9b
7 changed files with 67 additions and 97 deletions

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------*/
import React, { useState, useEffect, useCallback } from 'react'
import { MCPServerState, RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
import { MCPUserState, RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'

View file

@ -910,6 +910,9 @@ const MCPServer = ({ name, server }: { name: string, server: MCPServerObject })
mcpService.toggleMCPServer(name, e);
}
const voidSettings = useSettingsState()
const isOn = voidSettings.mcpUserStateOfName[name]?.isOn
return (
<div className="border-b border-gray-800 bg-gray-300/10 py-4 rounded-lg ">
<div className="flex items-center mx-4">
@ -929,7 +932,7 @@ const MCPServer = ({ name, server }: { name: string, server: MCPServerObject })
{/* Power toggle switch */}
<div className="ml-auto">
<VoidSwitch
value={server.isOn ?? false}
value={isOn ?? false}
disabled={server.status === 'error'}
onChange={handleChangeEvent}
/>
@ -939,7 +942,7 @@ const MCPServer = ({ name, server }: { name: string, server: MCPServerObject })
{/* Tools section */}
<div className="mt-1 mx-4">
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto pb-1">
{server.isOn && server.tools.length > 0 ? (
{isOn && server.tools.length > 0 ? (
server.tools.map((tool: { name: string; description?: string }) => (
<span
key={tool.name}
@ -956,7 +959,7 @@ const MCPServer = ({ name, server }: { name: string, server: MCPServerObject })
</div>
{/* Command badge */}
{server.isOn && server.command && (
{isOn && server.command && (
<div className="mt-2 mx-4">
<div className="text-xs text-gray-400">Command:</div>
<div className="px-2 py-1 bg-void-bg-3 text-xs font-mono overflow-x-auto whitespace-nowrap">
@ -983,11 +986,6 @@ const MCPServer = ({ name, server }: { name: string, server: MCPServerObject })
// Main component that renders the list of servers
const MCPServersList = () => {
const mcpServiceState = useMCPServiceState()
const accessor = useAccessor();
const userSpecifiedMCPServerNames = mcpServiceState.userSpecifiedMCPServerNames
// TODO tell the user what servers they've specified (might be different from those found)
let content: React.ReactNode
if (mcpServiceState.error) {

View file

@ -14,17 +14,16 @@ import { IProductService } from '../../../../platform/product/common/productServ
import { VSBuffer } from '../../../../base/common/buffer.js';
import { IChannel } from '../../../../base/parts/ipc/common/ipc.js';
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
import { MCPServerOfName, MCPConfigFileType, MCPAddServerResponse, MCPUpdateServerResponse, MCPDeleteServerResponse, MCPServerEventResponse, MCPServerObject, MCPToolCallParams, MCPGenericToolResponse } from './mcpServiceTypes.js';
import { MCPServerOfName, MCPConfigFileType, MCPAddServerResponse, MCPUpdateServerResponse, MCPDeleteServerResponse, MCPServerObject, MCPToolCallParams, MCPGenericToolResponse } from './mcpServiceTypes.js';
import { Event, Emitter } from '../../../../base/common/event.js';
import { InternalToolInfo } from './prompt/prompts.js';
import { IVoidSettingsService } from './voidSettingsService.js';
import { MCPServerStateOfName } from './voidSettingsTypes.js';
import { MCPUserStateOfName } from './voidSettingsTypes.js';
type MCPState = {
mcpServerOfName: MCPServerOfName,
error: string | undefined,
userSpecifiedMCPServerNames: string[],
error: string | undefined, // global parsing error
}
export interface IMCPService {
@ -77,7 +76,6 @@ class MCPService extends Disposable implements IMCPService {
state: MCPState = {
mcpServerOfName: {},
error: undefined,
userSpecifiedMCPServerNames: [],
}
// Emitters for server events
@ -96,15 +94,12 @@ class MCPService extends Disposable implements IMCPService {
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
) {
super();
// Register the service with the instantiation service
this.channel = this.mainProcessService.getChannel('void-channel-mcp')
// Register listeners for the channel
this._register((this.channel.listen('onAdd_server') satisfies Event<MCPAddServerResponse>)(e => this._onGetServerEvent(e)));
this._register((this.channel.listen('onUpdate_server') satisfies Event<MCPUpdateServerResponse>)(e => this._onGetServerEvent(e)));
this._register((this.channel.listen('onDelete_server') satisfies Event<MCPDeleteServerResponse>)(e => this._onGetServerEvent(e)));
// this._register((this.channel.listen('onLoading_server') satisfies Event<MCPServerEventLoadingParam>)(e => this._onServerEvent(e)));
// Initialize the service
this._register((this.channel.listen('onAdd_server') satisfies Event<MCPAddServerResponse>)(e => { this._setMCPServerState(e.response.name, e.response.newServer) }));
this._register((this.channel.listen('onUpdate_server') satisfies Event<MCPUpdateServerResponse>)(e => { this._setMCPServerState(e.response.name, e.response.newServer) }));
this._register((this.channel.listen('onDelete_server') satisfies Event<MCPDeleteServerResponse>)(e => { this._setMCPServerState(e.response.name, e.response.newServer) }));
this._initialize();
}
@ -127,12 +122,7 @@ class MCPService extends Disposable implements IMCPService {
}
}
private async _onGetServerEvent(e: MCPServerEventResponse) {
this._setMCPServer(e.response.name, e.response.newServer)
}
private readonly _setMCPServer = async (serverName: string, newServer: MCPServerObject | undefined) => {
private readonly _setMCPServerState = async (serverName: string, newServer: MCPServerObject | undefined) => {
this.state = {
...this.state,
mcpServerOfName: {
@ -261,49 +251,37 @@ class MCPService extends Disposable implements IMCPService {
private async _refreshMCPServers(): Promise<void> {
this._setHasError(undefined)
// TODO!!! set is loading
const mcpConfigFile = await this._parseMCPConfigFile();
if (!mcpConfigFile) { console.log(`Not setting state: MCP config file not found`); return }
if (!mcpConfigFile?.mcpServers) { console.log(`Not setting state: MCP config file did not have an 'mcpServers' field`); return }
const newConfigFileJSON = await this._parseMCPConfigFile();
if (!newConfigFileJSON) { console.log(`Not setting state: MCP config file not found`); return }
if (!newConfigFileJSON?.mcpServers) { console.log(`Not setting state: MCP config file did not have an 'mcpServers' field`); return }
// set state to loading if it's the first time we're seeing it
const mcpConfigOfName = mcpConfigFile.mcpServers
const oldConfigFileNames = Object.keys(this.state.mcpServerOfName)
const newConfigFileNames = Object.keys(newConfigFileJSON.mcpServers)
const addedServerNames = newConfigFileNames.filter(serverName => !oldConfigFileNames.includes(serverName)); // in new and not in old
const removedServerNames = oldConfigFileNames.filter(serverName => !newConfigFileNames.includes(serverName)); // in old and not in new
// set isOn to any new servers in the config
const addedUserStateOfName: MCPUserStateOfName = {}
for (const name in addedServerNames) { addedUserStateOfName[name] = { isOn: true } }
await this.voidSettingsService.addMCPUserStateOfNames(addedUserStateOfName);
// delete isOn for any servers that no longer show up in the config
await this.voidSettingsService.removeMCPUserStateOfNames(removedServerNames);
// set all servers to loading
const mcpConfigOfName = newConfigFileJSON.mcpServers
for (const serverName in mcpConfigOfName) {
if (serverName in this.state.mcpServerOfName) continue
this._setMCPServer(serverName, {
isOn: false,
this._setMCPServerState(serverName, {
status: 'loading',
error: undefined,
command: undefined,
tools: [],
})
}
const currMCPStateOfName = this.voidSettingsService.state.mcpServerStateOfName;
const availableServers = Object.keys(mcpConfigFile.mcpServers);
// Handle added servers
const addedServers = availableServers.filter(serverName => !currMCPStateOfName[serverName]?.isOn);
const addedServersObject = addedServers.reduce((acc, serverName) => {
acc[serverName] = { isOn: true };
return acc;
}, {} as MCPServerStateOfName);
await this.voidSettingsService.addMCPServerStateOfName(addedServersObject);
// Handle removed servers
const removedServers = Object.keys(currMCPStateOfName).filter(serverName => availableServers.indexOf(serverName) === -1);
await this.voidSettingsService.removeMCPServerStateNames(removedServers);
// Compile the updated server list as MCPServerStates
const updatedServers = Object.keys(currMCPStateOfName).reduce((acc, serverName) => {
if (availableServers.includes(serverName)) {
acc[serverName] = currMCPStateOfName[serverName];
}
return acc;
}, {} as MCPServerStateOfName);
this.channel.call('refreshMCPServers', { mcpConfig: mcpConfigFile, serverStates: updatedServers })
this.channel.call('refreshMCPServers', { mcpConfig: newConfigFileJSON, userStateOfName: this.voidSettingsService.state.mcpUserStateOfName })
}
public async callMCPTool(toolData: MCPToolCallParams): Promise<MCPGenericToolResponse> {

View file

@ -138,7 +138,6 @@ export interface MCPServerObject {
// Command-based server properties
tools: MCPTool[],
status: 'loading' | 'error' | 'success' | 'offline',
isOn: boolean | undefined,
command?: string,
error?: string,
}

View file

@ -13,7 +13,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
import { IMetricsService } from './metricsService.js';
import { defaultProviderSettings, getModelCapabilities, ModelOverrides } from './modelCapabilities.js';
import { VOID_SETTINGS_STORAGE_KEY } from './storageKeys.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode, OverridesOfModel, defaultOverridesOfModel, MCPServerStateOfName, MCPServerState } from './voidSettingsTypes.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode, OverridesOfModel, defaultOverridesOfModel, MCPUserStateOfName as MCPUserStateOfName, MCPUserState } from './voidSettingsTypes.js';
// name is the name in the dropdown
@ -43,7 +43,7 @@ export type VoidSettingsState = {
readonly optionsOfModelSelection: OptionsOfModelSelection;
readonly overridesOfModel: OverridesOfModel;
readonly globalSettings: GlobalSettings;
readonly mcpServerStateOfName: MCPServerStateOfName;
readonly mcpUserStateOfName: MCPUserStateOfName; // user-controlled state of MCP servers
readonly _modelOptions: ModelOption[] // computed based on the two above items
}
@ -76,9 +76,9 @@ export interface IVoidSettingsService {
addModel(providerName: ProviderName, modelName: string): void;
deleteModel(providerName: ProviderName, modelName: string): boolean;
addMCPServerStateOfName(serverStateOfName: MCPServerStateOfName): Promise<void>;
removeMCPServerStateNames(serverNames: string[]): Promise<void>;
setMCPServerState(serverName: string, state: MCPServerState): Promise<void>;
addMCPUserStateOfNames(userStateOfName: MCPUserStateOfName): Promise<void>;
removeMCPUserStateOfNames(serverNames: string[]): Promise<void>;
setMCPServerState(serverName: string, state: MCPUserState): Promise<void>;
}
@ -218,7 +218,7 @@ const defaultState = () => {
optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {} },
overridesOfModel: deepClone(defaultOverridesOfModel),
_modelOptions: [], // computed later
mcpServerStateOfName: {},
mcpUserStateOfName: {},
}
return d
}
@ -368,7 +368,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
const newGlobalSettings = this.state.globalSettings
const newOverridesOfModel = this.state.overridesOfModel
const newMCPServerStateOfName = this.state.mcpServerStateOfName
const newMCPUserStateOfName = this.state.mcpUserStateOfName
const newState = {
modelSelectionOfFeature: newModelSelectionOfFeature,
@ -376,7 +376,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
settingsOfProvider: newSettingsOfProvider,
globalSettings: newGlobalSettings,
overridesOfModel: newOverridesOfModel,
mcpServerStateOfName: newMCPServerStateOfName,
mcpUserStateOfName: newMCPUserStateOfName,
}
this.state = _validatedModelState(newState)
@ -541,11 +541,11 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
}
// MCP Server State
private _setMCPServerStateOfName = async (newStates: MCPServerStateOfName) => {
private _setMCPUserStateOfName = async (newStates: MCPUserStateOfName) => {
const newState: VoidSettingsState = {
...this.state,
mcpServerStateOfName: {
...this.state.mcpServerStateOfName,
mcpUserStateOfName: {
...this.state.mcpUserStateOfName,
...newStates
}
};
@ -555,18 +555,18 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
this._metricsService.capture('Set MCP Server States', { newStates });
}
addMCPServerStateOfName = async (newMCPStates: MCPServerStateOfName) => {
const { mcpServerStateOfName: mcpServerStates } = this.state
addMCPUserStateOfNames = async (newMCPStates: MCPUserStateOfName) => {
const { mcpUserStateOfName: mcpServerStates } = this.state
const newMCPServerStates = {
...mcpServerStates,
...newMCPStates,
}
await this._setMCPServerStateOfName(newMCPServerStates)
this._metricsService.capture('Add MCP Server', { servers: Object.keys(newMCPStates).join(', ') });
await this._setMCPUserStateOfName(newMCPServerStates)
this._metricsService.capture('Add MCP Servers', { servers: Object.keys(newMCPStates).join(', ') });
}
removeMCPServerStateNames = async (serverNames: string[]) => {
const { mcpServerStateOfName: mcpServerStates } = this.state
removeMCPUserStateOfNames = async (serverNames: string[]) => {
const { mcpUserStateOfName: mcpServerStates } = this.state
const newMCPServerStates = {
...mcpServerStates,
}
@ -575,18 +575,18 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
delete newMCPServerStates[serverName]
}
})
await this._setMCPServerStateOfName(newMCPServerStates)
this._metricsService.capture('Remove MCP Server', { servers: serverNames.join(', ') });
await this._setMCPUserStateOfName(newMCPServerStates)
this._metricsService.capture('Remove MCP Servers', { servers: serverNames.join(', ') });
}
setMCPServerState = async (serverName: string, state: MCPServerState) => {
const { mcpServerStateOfName: mcpServerStates } = this.state
setMCPServerState = async (serverName: string, state: MCPUserState) => {
const { mcpUserStateOfName: mcpServerStates } = this.state
if (!(serverName in mcpServerStates)) return // if not in list, do nothing
const newMCPServerStates = {
...mcpServerStates,
[serverName]: state,
}
await this._setMCPServerStateOfName(newMCPServerStates)
await this._setMCPUserStateOfName(newMCPServerStates)
this._metricsService.capture('Update MCP Server State', { serverName, state });
}

View file

@ -494,10 +494,10 @@ export const defaultOverridesOfModel = overridesOfModel
export interface MCPServerStateOfName {
[serverName: string]: MCPServerState | undefined;
export interface MCPUserStateOfName {
[serverName: string]: MCPUserState | undefined;
}
export interface MCPServerState {
export interface MCPUserState {
isOn: boolean;
}

View file

@ -17,7 +17,7 @@ import { MCPConfigFileType, MCPConfigFileServerType, MCPServerErrorModel, MCPSer
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { equals } from '../../../../base/common/objects.js';
import { MCPServerStateOfName } from '../common/voidSettingsTypes.js';
import { MCPUserStateOfName } from '../common/voidSettingsTypes.js';
// const getLoadingServerObject = (serverName: string, isOn: boolean | undefined) => {
@ -118,9 +118,9 @@ export class MCPChannel implements IServerChannel {
// server functions
private async _refreshMCPServers(params: { mcpConfig: MCPConfigFileType, serverStates: MCPServerStateOfName }) {
private async _refreshMCPServers(params: { mcpConfig: MCPConfigFileType, userStateOfName: MCPUserStateOfName }) {
const { mcpConfig, serverStates } = params
const { mcpConfig, userStateOfName } = params
// Get all prevServers
const prevServers = { ...this.clients }
@ -174,7 +174,7 @@ export class MCPChannel implements IServerChannel {
if (addedServers.length > 0) {
// emit added servers
const addPromises = addedServers.map(async (serverName) => {
const addedServer = await this._safeSetupServer(mcpServers[serverName], serverName, serverStates[serverName]?.isOn)
const addedServer = await this._safeSetupServer(mcpServers[serverName], serverName, userStateOfName[serverName]?.isOn)
return {
type: 'add',
newServer: addedServer,
@ -189,7 +189,7 @@ export class MCPChannel implements IServerChannel {
// emit updated servers
const updatePromises = updatedServers.map(async (serverName) => {
const prevServer = this.clients[serverName]?.formattedServer;
const newServer = await this._safeSetupServer(mcpServers[serverName], serverName, serverStates[serverName]?.isOn)
const newServer = await this._safeSetupServer(mcpServers[serverName], serverName, userStateOfName[serverName]?.isOn)
return {
type: 'update',
prevServer,
@ -234,7 +234,6 @@ export class MCPChannel implements IServerChannel {
const { tools } = await client.listTools()
formattedServer = {
status: isOn ? 'success' : 'offline',
isOn,
tools: tools,
command: server.url.toString(),
}
@ -245,7 +244,6 @@ export class MCPChannel implements IServerChannel {
console.log(`Connected via SSE to ${serverName}`);
formattedServer = {
status: isOn ? 'success' : 'offline',
isOn,
tools: [],
command: server.url.toString(),
}
@ -272,7 +270,6 @@ export class MCPChannel implements IServerChannel {
// Format server object
formattedServer = {
status: isOn ? 'success' : 'offline',
isOn,
tools: tools,
command: fullCommand,
}
@ -301,7 +298,6 @@ export class MCPChannel implements IServerChannel {
const formattedError: MCPServerErrorModel = {
status: 'error',
isOn: false,
tools: [],
error: typedErr.message,
command: fullCommand,
@ -369,7 +365,6 @@ export class MCPChannel implements IServerChannel {
name: serverName,
newServer: {
status: 'offline',
isOn,
tools: [],
command: '',
// Explicitly set error to undefined