feat: add McpDropdown component with related UI enhancements
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import type { McpStatus } from '@opencode-ai/sdk/v2';
|
||||
import { opencodeClient } from '@/lib/opencode/client';
|
||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
|
||||
export type McpStatusMap = Record<string, McpStatus>;
|
||||
|
||||
const EMPTY_STATUS: McpStatusMap = {};
|
||||
|
||||
type McpHealth = {
|
||||
connected: number;
|
||||
total: number;
|
||||
hasFailed: boolean;
|
||||
hasAuthRequired: boolean;
|
||||
};
|
||||
|
||||
const normalizeDirectory = (directory: string | null | undefined): string | null => {
|
||||
if (typeof directory !== 'string') return null;
|
||||
const trimmed = directory.trim();
|
||||
if (!trimmed) return null;
|
||||
const normalized = trimmed.replace(/\\/g, '/');
|
||||
return normalized.length > 1 ? normalized.replace(/\/+$/, '') : normalized;
|
||||
};
|
||||
|
||||
const toKey = (directory: string | null | undefined): string => normalizeDirectory(directory) ?? '__global__';
|
||||
|
||||
export const computeMcpHealth = (status: McpStatusMap | null | undefined): McpHealth => {
|
||||
const entries = Object.entries(status ?? {});
|
||||
const connected = entries.filter(([, s]) => s?.status === 'connected').length;
|
||||
const total = entries.length;
|
||||
const hasFailed = entries.some(([, s]) => s?.status === 'failed');
|
||||
const hasAuthRequired = entries.some(([, s]) => s?.status === 'needs_auth' || s?.status === 'needs_client_registration');
|
||||
return { connected, total, hasFailed, hasAuthRequired };
|
||||
};
|
||||
|
||||
type RefreshOptions = {
|
||||
directory?: string | null;
|
||||
silent?: boolean;
|
||||
};
|
||||
|
||||
interface McpStore {
|
||||
byDirectory: Record<string, McpStatusMap>;
|
||||
loadingKeys: Record<string, boolean>;
|
||||
lastErrorKeys: Record<string, string | null>;
|
||||
|
||||
getStatusForDirectory: (directory?: string | null) => McpStatusMap;
|
||||
refresh: (options?: RefreshOptions) => Promise<void>;
|
||||
connect: (name: string, directory?: string | null) => Promise<void>;
|
||||
disconnect: (name: string, directory?: string | null) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useMcpStore = create<McpStore>()(
|
||||
devtools((set, get) => ({
|
||||
byDirectory: {},
|
||||
loadingKeys: {},
|
||||
lastErrorKeys: {},
|
||||
|
||||
getStatusForDirectory: (directory) => {
|
||||
const key = toKey(directory ?? useDirectoryStore.getState().currentDirectory);
|
||||
return get().byDirectory[key] ?? EMPTY_STATUS;
|
||||
},
|
||||
|
||||
refresh: async (options) => {
|
||||
const directory = normalizeDirectory(options?.directory ?? useDirectoryStore.getState().currentDirectory);
|
||||
const key = toKey(directory);
|
||||
|
||||
if (!options?.silent) {
|
||||
set((state) => ({
|
||||
loadingKeys: { ...state.loadingKeys, [key]: true },
|
||||
lastErrorKeys: { ...state.lastErrorKeys, [key]: null },
|
||||
}));
|
||||
}
|
||||
|
||||
try {
|
||||
const api = opencodeClient.getApiClient();
|
||||
const result = await api.mcp.status(directory ? { directory } : undefined);
|
||||
const data = (result.data ?? {}) as McpStatusMap;
|
||||
|
||||
set((state) => ({
|
||||
byDirectory: { ...state.byDirectory, [key]: data },
|
||||
loadingKeys: { ...state.loadingKeys, [key]: false },
|
||||
lastErrorKeys: { ...state.lastErrorKeys, [key]: null },
|
||||
}));
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to load MCP status';
|
||||
set((state) => ({
|
||||
loadingKeys: { ...state.loadingKeys, [key]: false },
|
||||
lastErrorKeys: { ...state.lastErrorKeys, [key]: message },
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
connect: async (name, directory) => {
|
||||
const normalized = normalizeDirectory(directory ?? useDirectoryStore.getState().currentDirectory);
|
||||
const api = opencodeClient.getApiClient();
|
||||
await api.mcp.connect({ name, ...(normalized ? { directory: normalized } : {}) }, { throwOnError: true });
|
||||
await get().refresh({ directory: normalized, silent: true });
|
||||
},
|
||||
|
||||
disconnect: async (name, directory) => {
|
||||
const normalized = normalizeDirectory(directory ?? useDirectoryStore.getState().currentDirectory);
|
||||
const api = opencodeClient.getApiClient();
|
||||
await api.mcp.disconnect({ name, ...(normalized ? { directory: normalized } : {}) }, { throwOnError: true });
|
||||
await get().refresh({ directory: normalized, silent: true });
|
||||
},
|
||||
|
||||
}))
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user