diff --git a/src/renderer/components/extensions/common/InstallButton.tsx b/src/renderer/components/extensions/common/InstallButton.tsx
index 2bc48112..78930ad7 100644
--- a/src/renderer/components/extensions/common/InstallButton.tsx
+++ b/src/renderer/components/extensions/common/InstallButton.tsx
@@ -24,6 +24,7 @@ interface InstallButtonProps {
isInstalled: boolean;
onInstall: () => void;
onUninstall: () => void;
+ section?: 'plugins' | 'mcp';
disabled?: boolean;
size?: 'sm' | 'default';
errorMessage?: string;
@@ -34,6 +35,7 @@ export const InstallButton = ({
isInstalled,
onInstall,
onUninstall,
+ section = 'plugins',
disabled,
size = 'sm',
errorMessage,
@@ -48,6 +50,7 @@ export const InstallButton = ({
isInstalled,
cliStatus,
cliStatusLoading,
+ section,
});
const isDisabled = disabled || Boolean(disableReason);
const [lastAction, setLastAction] = useState<'install' | 'uninstall' | null>(null);
diff --git a/src/renderer/components/extensions/mcp/McpServerCard.tsx b/src/renderer/components/extensions/mcp/McpServerCard.tsx
index afae2142..4f58b343 100644
--- a/src/renderer/components/extensions/mcp/McpServerCard.tsx
+++ b/src/renderer/components/extensions/mcp/McpServerCard.tsx
@@ -258,6 +258,7 @@ export const McpServerCard = ({
installMcpServer({
registryId: server.id,
diff --git a/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx b/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx
index d16e0885..412be5a8 100644
--- a/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx
+++ b/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx
@@ -528,6 +528,7 @@ export const McpServerDetailDialog = ({
({
browseCatalog: s.mcpBrowseCatalog,
@@ -105,6 +106,7 @@ export const McpServersPanel = ({
mcpDiagnosticsError: s.mcpDiagnosticsError,
mcpDiagnosticsLastCheckedAt: s.mcpDiagnosticsLastCheckedAt,
runMcpDiagnostics: s.runMcpDiagnostics,
+ cliStatus: s.cliStatus,
}))
);
@@ -118,8 +120,8 @@ export const McpServersPanel = ({
}, [browseCatalog.length, browseError, browseLoading, mcpBrowse]);
useEffect(() => {
- void runMcpDiagnostics();
- }, [runMcpDiagnostics]);
+ void runMcpDiagnostics(projectPath ?? undefined);
+ }, [projectPath, runMcpDiagnostics]);
// Fetch GitHub stars after catalog loads (fire-and-forget)
useEffect(() => {
@@ -185,6 +187,12 @@ export const McpServersPanel = ({
// Sort displayed servers
const displayServers = useMemo(() => sortMcpServers(rawServers, mcpSort), [rawServers, mcpSort]);
+ const runtimeLabel =
+ cliStatus?.flavor === 'agent_teams_orchestrator' ? 'multimodel runtime' : 'Claude CLI';
+ const diagnosticsCommand =
+ cliStatus?.flavor === 'agent_teams_orchestrator'
+ ? 'claude-multimodel mcp diagnose'
+ : 'claude mcp list';
// Find selected server (search in both lists to avoid losing selection during search toggle)
const selectedServer = useMemo(() => {
@@ -205,14 +213,12 @@ export const McpServersPanel = ({
MCP Health Status
{mcpDiagnosticsLoading ? (
- <>
- Checking installed MCP servers via Claude CLI (claude mcp list) ...
- >
+ <>Checking installed MCP servers via {runtimeLabel} ...>
) : mcpDiagnosticsLastCheckedAt ? (
`Last checked ${formatRelativeTime(new Date(mcpDiagnosticsLastCheckedAt).toISOString())}`
) : (
<>
- Run diagnostics (claude mcp list) to verify installed MCP
+ Run diagnostics ({diagnosticsCommand}) to verify installed MCP
connectivity.
>
)}
@@ -221,7 +227,7 @@ export const McpServersPanel = ({
void runMcpDiagnostics()}
+ onClick={() => void runMcpDiagnostics(projectPath ?? undefined)}
disabled={mcpDiagnosticsLoading}
className="whitespace-nowrap"
>
@@ -235,7 +241,7 @@ export const McpServersPanel = ({
{(mcpDiagnosticsLoading || allDiagnostics.length > 0) && (
-
Claude MCP List Results
+
Runtime MCP Diagnostics
{allDiagnostics.length > 0 && (
{allDiagnostics.length} servers
)}
diff --git a/src/renderer/components/extensions/plugins/PluginCard.tsx b/src/renderer/components/extensions/plugins/PluginCard.tsx
index deef088f..320d3e15 100644
--- a/src/renderer/components/extensions/plugins/PluginCard.tsx
+++ b/src/renderer/components/extensions/plugins/PluginCard.tsx
@@ -119,6 +119,7 @@ export const PluginCard = ({ plugin, index, onClick }: PluginCardProps): React.J
installPlugin({ pluginId: plugin.pluginId, scope: 'user' })}
onUninstall={() => uninstallPlugin(plugin.pluginId, 'user')}
size="sm"
diff --git a/src/renderer/components/extensions/plugins/PluginDetailDialog.tsx b/src/renderer/components/extensions/plugins/PluginDetailDialog.tsx
index b8440789..298788c7 100644
--- a/src/renderer/components/extensions/plugins/PluginDetailDialog.tsx
+++ b/src/renderer/components/extensions/plugins/PluginDetailDialog.tsx
@@ -195,6 +195,7 @@ export const PluginDetailDialog = ({
installPlugin({
pluginId: plugin.pluginId,
diff --git a/src/renderer/components/extensions/plugins/PluginsPanel.tsx b/src/renderer/components/extensions/plugins/PluginsPanel.tsx
index 4fe38a2e..0f7cf218 100644
--- a/src/renderer/components/extensions/plugins/PluginsPanel.tsx
+++ b/src/renderer/components/extensions/plugins/PluginsPanel.tsx
@@ -16,6 +16,7 @@ import {
SelectValue,
} from '@renderer/components/ui/select';
import { useStore } from '@renderer/store';
+import { getCliProviderExtensionCapability } from '@shared/utils/providerExtensionCapabilities';
import { inferCapabilities, normalizeCategory } from '@shared/utils/extensionNormalizers';
import { ArrowUpDown, Filter, Puzzle, Search } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow';
@@ -125,11 +126,12 @@ export const PluginsPanel = ({
hasActiveFilters,
setPluginSort,
}: PluginsPanelProps): React.JSX.Element => {
- const { catalog, loading, error } = useStore(
+ const { catalog, loading, error, cliStatus } = useStore(
useShallow((s) => ({
catalog: s.pluginCatalog,
loading: s.pluginCatalogLoading,
error: s.pluginCatalogError,
+ cliStatus: s.cliStatus,
}))
);
@@ -175,9 +177,26 @@ export const PluginsPanel = ({
}
return counts.size;
}, [catalog]);
-
return (
+ {cliStatus?.flavor === 'agent_teams_orchestrator' &&
+ (() => {
+ const codexProvider = cliStatus.providers.find(
+ (provider) => provider.providerId === 'codex'
+ );
+ if (!codexProvider) return null;
+ const capability = getCliProviderExtensionCapability(codexProvider, 'plugins');
+ if (capability.status === 'supported') {
+ return null;
+ }
+
+ return (
+
+ Plugins currently apply to Anthropic sessions in the multimodel runtime.
+ {capability.reason ? ` ${capability.reason}` : ''}
+
+ );
+ })()}
{/* Search + Sort + Installed only row */}
diff --git a/src/renderer/components/extensions/skills/SkillsPanel.tsx b/src/renderer/components/extensions/skills/SkillsPanel.tsx
index faf7e193..07262337 100644
--- a/src/renderer/components/extensions/skills/SkillsPanel.tsx
+++ b/src/renderer/components/extensions/skills/SkillsPanel.tsx
@@ -94,6 +94,7 @@ export const SkillsPanel = ({
const catalogKey = projectPath ?? USER_SKILLS_CATALOG_KEY;
const fetchSkillsCatalog = useStore((s) => s.fetchSkillsCatalog);
const fetchSkillDetail = useStore((s) => s.fetchSkillDetail);
+ const cliStatus = useStore((s) => s.cliStatus);
const skillsLoading = useStore((s) => s.skillsCatalogLoadingByProjectPath[catalogKey] ?? false);
const skillsError = useStore((s) => s.skillsCatalogErrorByProjectPath[catalogKey] ?? null);
const detailById = useStore(useShallow((s) => s.skillsDetailsById));
@@ -226,6 +227,12 @@ export const SkillsPanel = ({
return (
+ {cliStatus?.flavor === 'agent_teams_orchestrator' && (
+
+ Skills are shared across providers in this runtime. A personal or project skill you edit
+ here is available to both Anthropic and Codex sessions that support skills.
+
+ )}
diff --git a/src/renderer/store/slices/extensionsSlice.ts b/src/renderer/store/slices/extensionsSlice.ts
index 6f56629b..e1ed7a2d 100644
--- a/src/renderer/store/slices/extensionsSlice.ts
+++ b/src/renderer/store/slices/extensionsSlice.ts
@@ -90,7 +90,7 @@ export interface ExtensionsSlice {
fetchPluginReadme: (pluginId: string) => void;
mcpBrowse: (cursor?: string) => Promise
;
mcpFetchInstalled: (projectPath?: string) => Promise;
- runMcpDiagnostics: () => Promise;
+ runMcpDiagnostics: (projectPath?: string) => Promise;
fetchSkillsCatalog: (projectPath?: string) => Promise;
fetchSkillDetail: (skillId: string, projectPath?: string) => Promise;
previewSkillUpsert: (request: SkillUpsertRequest) => Promise;
@@ -375,7 +375,8 @@ export const createExtensionsSlice: StateCreator {
+ let currentPromise: Promise | null = null;
+ currentPromise = (async () => {
try {
const result = await api.plugins!.getAll(projectPath, forceRefresh);
set((prev) => {
@@ -431,14 +432,14 @@ export const createExtensionsSlice: StateCreator {
+ runMcpDiagnostics: async (projectPath?: string) => {
const mcpRegistry = api.mcpRegistry;
if (!mcpRegistry) return;
@@ -550,7 +551,7 @@ export const createExtensionsSlice: StateCreator {
try {
- const diagnostics = await mcpRegistry.diagnose();
+ const diagnostics = await mcpRegistry.diagnose(projectPath);
set({
mcpDiagnostics: Object.fromEntries(
diagnostics.map((entry) => [entry.name, entry] as const)
@@ -960,7 +961,7 @@ export const createExtensionsSlice: StateCreator ({
@@ -1008,7 +1009,7 @@ export const createExtensionsSlice: StateCreator ({
@@ -1064,7 +1065,7 @@ export const createExtensionsSlice: StateCreator ({
@@ -1116,7 +1117,7 @@ export const createExtensionsSlice: StateCreator ({
apiKeys: prev.apiKeys.filter((k) => k.id !== id),
}));
diff --git a/src/shared/utils/extensionNormalizers.ts b/src/shared/utils/extensionNormalizers.ts
index acdf3a88..4bb90652 100644
--- a/src/shared/utils/extensionNormalizers.ts
+++ b/src/shared/utils/extensionNormalizers.ts
@@ -2,6 +2,11 @@
* Pure-function normalizers for Extension Store data.
*/
+import {
+ getCliProviderExtensionCapability,
+ isCliExtensionCapabilityMutable,
+} from './providerExtensionCapabilities';
+
import type {
CliInstallationStatus,
InstallScope,
@@ -208,28 +213,74 @@ export function getExtensionActionDisableReason(options: {
isInstalled: boolean;
cliStatus: Pick<
CliInstallationStatus,
- 'installed' | 'authLoggedIn' | 'binaryPath' | 'launchError'
+ 'installed' | 'authLoggedIn' | 'binaryPath' | 'launchError' | 'flavor' | 'providers'
> | null;
cliStatusLoading: boolean;
+ section?: 'plugins' | 'mcp';
}): string | null {
- const { isInstalled, cliStatus, cliStatusLoading } = options;
+ const { isInstalled, cliStatus, cliStatusLoading, section = 'plugins' } = options;
if (cliStatusLoading) {
- return 'Checking Claude CLI status...';
+ return 'Checking runtime status...';
}
if (cliStatus === null) {
- return 'Checking Claude CLI availability...';
+ return 'Checking runtime availability...';
}
if (cliStatus.installed === false) {
if (cliStatus.binaryPath && cliStatus.launchError) {
- return 'Claude CLI was found but failed to start. Open the Dashboard to repair or reinstall it.';
+ return 'The configured runtime was found but failed to start. Open the Dashboard to repair or reinstall it.';
}
- return 'Claude CLI required. Install it from the Dashboard.';
+ return 'The configured runtime is required. Install or repair it from the Dashboard.';
}
- if (!isInstalled && !cliStatus.authLoggedIn) {
- return 'Claude CLI is installed but not signed in. Open the Dashboard to sign in.';
+ const providers = cliStatus.providers ?? [];
+ const isMultimodel = cliStatus.flavor === 'agent_teams_orchestrator' && providers.length > 0;
+
+ if (section === 'mcp') {
+ if (!isMultimodel) {
+ return null;
+ }
+
+ const mutableProviders = providers.filter((provider) =>
+ isCliExtensionCapabilityMutable(getCliProviderExtensionCapability(provider, 'mcp'))
+ );
+ if (mutableProviders.length > 0) {
+ return null;
+ }
+
+ const reason = providers
+ .map((provider) => getCliProviderExtensionCapability(provider, 'mcp').reason)
+ .find((value): value is string => typeof value === 'string' && value.trim().length > 0);
+
+ return reason ?? 'MCP management is not supported by the current runtime.';
+ }
+
+ if (!isMultimodel) {
+ if (!isInstalled && !cliStatus.authLoggedIn) {
+ return 'Claude CLI is installed but not signed in. Open the Dashboard to sign in.';
+ }
+ return null;
+ }
+
+ const pluginProviders = providers.filter((provider) =>
+ isCliExtensionCapabilityMutable(getCliProviderExtensionCapability(provider, 'plugins'))
+ );
+
+ if (pluginProviders.length === 0) {
+ const reason = providers
+ .map((provider) => getCliProviderExtensionCapability(provider, 'plugins').reason)
+ .find((value): value is string => typeof value === 'string' && value.trim().length > 0);
+ return reason ?? 'Plugin installs are not supported by the current runtime.';
+ }
+
+ if (isInstalled) {
+ return null;
+ }
+
+ const authenticatedProvider = pluginProviders.find((provider) => provider.authenticated);
+ if (!authenticatedProvider) {
+ return `${pluginProviders[0]?.displayName ?? 'Anthropic'} is not connected. Open the Dashboard to sign in.`;
}
return null;
diff --git a/test/renderer/components/extensions/plugins/PluginDetailDialog.test.ts b/test/renderer/components/extensions/plugins/PluginDetailDialog.test.ts
index 753761f8..8e47ecfb 100644
--- a/test/renderer/components/extensions/plugins/PluginDetailDialog.test.ts
+++ b/test/renderer/components/extensions/plugins/PluginDetailDialog.test.ts
@@ -258,8 +258,12 @@ describe('PluginDetailDialog project context', () => {
const scopeSelect = host.querySelector('[data-testid="scope-select"]') as HTMLSelectElement;
expect(scopeSelect).not.toBeNull();
- expect(scopeSelect.querySelector('option[value="project"]')?.disabled).toBe(true);
- expect(scopeSelect.querySelector('option[value="local"]')?.disabled).toBe(true);
+ expect(
+ (scopeSelect.querySelector('option[value="project"]') as HTMLOptionElement | null)?.disabled
+ ).toBe(true);
+ expect(
+ (scopeSelect.querySelector('option[value="local"]') as HTMLOptionElement | null)?.disabled
+ ).toBe(true);
await act(async () => {
root.unmount();
diff --git a/test/renderer/store/extensionsSlice.test.ts b/test/renderer/store/extensionsSlice.test.ts
index 16733b56..ceca353d 100644
--- a/test/renderer/store/extensionsSlice.test.ts
+++ b/test/renderer/store/extensionsSlice.test.ts
@@ -36,6 +36,20 @@ vi.mock('../../../src/renderer/api', () => ({
stopWatching: vi.fn(),
onChanged: vi.fn(),
},
+ apiKeys: {
+ list: vi.fn(),
+ save: vi.fn(),
+ delete: vi.fn(),
+ lookup: vi.fn(),
+ getStorageStatus: vi.fn(),
+ },
+ cliInstaller: {
+ getStatus: vi.fn(),
+ getProviderStatus: vi.fn(),
+ verifyProviderModels: vi.fn(),
+ invalidateStatus: vi.fn(),
+ onProgress: vi.fn(),
+ },
},
}));
@@ -148,6 +162,7 @@ describe('extensionsSlice', () => {
beforeEach(() => {
store = createTestStore();
vi.clearAllMocks();
+ (api.cliInstaller!.getStatus as ReturnType).mockResolvedValue(makeReadyCliStatus());
});
afterEach(() => {
@@ -765,6 +780,61 @@ describe('extensionsSlice', () => {
});
});
+ describe('provider-aware runtime refresh', () => {
+ it('passes projectPath through MCP diagnostics', async () => {
+ (api.mcpRegistry!.diagnose as ReturnType).mockResolvedValue([]);
+
+ await store.getState().runMcpDiagnostics('/tmp/project-a');
+
+ expect(api.mcpRegistry!.diagnose).toHaveBeenCalledWith('/tmp/project-a');
+ });
+
+ it('refreshes CLI status after saving an API key', async () => {
+ (api.apiKeys!.save as ReturnType).mockResolvedValue({ id: 'k1' });
+ (api.apiKeys!.list as ReturnType).mockResolvedValue([
+ {
+ id: 'k1',
+ name: 'Codex key',
+ envVarName: 'OPENAI_API_KEY',
+ maskedValue: '***',
+ scope: 'user',
+ createdAt: '2026-04-17T10:00:00.000Z',
+ },
+ ]);
+
+ await store.getState().saveApiKey({
+ name: 'Codex key',
+ envVarName: 'OPENAI_API_KEY',
+ value: 'secret',
+ scope: 'user',
+ });
+
+ expect(api.cliInstaller!.getStatus).toHaveBeenCalled();
+ expect(store.getState().apiKeys).toHaveLength(1);
+ });
+
+ it('refreshes CLI status after deleting an API key', async () => {
+ store.setState({
+ apiKeys: [
+ {
+ id: 'k1',
+ name: 'Codex key',
+ envVarName: 'OPENAI_API_KEY',
+ maskedValue: '***',
+ scope: 'user',
+ createdAt: '2026-04-17T10:00:00.000Z',
+ },
+ ],
+ });
+ (api.apiKeys!.delete as ReturnType).mockResolvedValue(undefined);
+
+ await store.getState().deleteApiKey('k1');
+
+ expect(api.cliInstaller!.getStatus).toHaveBeenCalled();
+ expect(store.getState().apiKeys).toEqual([]);
+ });
+ });
+
describe('skills state hardening', () => {
it('ignores stale catalog responses for the same project key', async () => {
let resolveFirst!: (value: SkillCatalogItem[]) => void;
diff --git a/test/shared/utils/extensionNormalizers.test.ts b/test/shared/utils/extensionNormalizers.test.ts
index 087d1f07..39ba92fc 100644
--- a/test/shared/utils/extensionNormalizers.test.ts
+++ b/test/shared/utils/extensionNormalizers.test.ts
@@ -252,11 +252,28 @@ describe('getMcpInstallationSummaryLabel', () => {
});
describe('getExtensionActionDisableReason', () => {
+ const createDirectCliStatus = (
+ overrides: Partial<{
+ installed: boolean;
+ authLoggedIn: boolean;
+ binaryPath: string | null;
+ launchError: string | null;
+ }> = {}
+ ) => ({
+ flavor: 'claude' as const,
+ installed: true,
+ authLoggedIn: true,
+ binaryPath: null,
+ launchError: null,
+ providers: [],
+ ...overrides,
+ });
+
it('requires auth only for install actions', () => {
expect(
getExtensionActionDisableReason({
isInstalled: false,
- cliStatus: { installed: true, authLoggedIn: false, binaryPath: null, launchError: null },
+ cliStatus: createDirectCliStatus({ authLoggedIn: false }),
cliStatusLoading: false,
}),
).toContain('not signed in');
@@ -266,7 +283,7 @@ describe('getExtensionActionDisableReason', () => {
expect(
getExtensionActionDisableReason({
isInstalled: true,
- cliStatus: { installed: true, authLoggedIn: false, binaryPath: null, launchError: null },
+ cliStatus: createDirectCliStatus({ authLoggedIn: false }),
cliStatusLoading: false,
}),
).toBeNull();
@@ -276,10 +293,10 @@ describe('getExtensionActionDisableReason', () => {
expect(
getExtensionActionDisableReason({
isInstalled: true,
- cliStatus: { installed: false, authLoggedIn: false, binaryPath: null, launchError: null },
+ cliStatus: createDirectCliStatus({ installed: false, authLoggedIn: false }),
cliStatusLoading: false,
}),
- ).toContain('Claude CLI required');
+ ).toContain('configured runtime');
});
it('surfaces startup health-check failures separately from missing CLI', () => {
@@ -287,15 +304,99 @@ describe('getExtensionActionDisableReason', () => {
getExtensionActionDisableReason({
isInstalled: false,
cliStatus: {
- installed: false,
- authLoggedIn: false,
- binaryPath: '/usr/local/bin/claude',
- launchError: 'spawn EACCES',
+ ...createDirectCliStatus({
+ installed: false,
+ authLoggedIn: false,
+ binaryPath: '/usr/local/bin/claude',
+ launchError: 'spawn EACCES',
+ }),
},
cliStatusLoading: false,
}),
).toContain('failed to start');
});
+
+ it('disables multimodel plugin installs when the runtime declares plugins unsupported', () => {
+ expect(
+ getExtensionActionDisableReason({
+ isInstalled: false,
+ section: 'plugins',
+ cliStatus: {
+ installed: true,
+ authLoggedIn: true,
+ binaryPath: '/usr/local/bin/claude-multimodel',
+ launchError: null,
+ flavor: 'agent_teams_orchestrator',
+ providers: [
+ {
+ providerId: 'anthropic',
+ displayName: 'Anthropic',
+ supported: true,
+ authenticated: false,
+ authMethod: null,
+ verificationState: 'unknown',
+ models: [],
+ canLoginFromUi: true,
+ capabilities: {
+ teamLaunch: true,
+ oneShot: true,
+ extensions: {
+ plugins: {
+ status: 'unsupported',
+ ownership: 'shared',
+ reason: 'Anthropic plugins unavailable',
+ },
+ mcp: { status: 'supported', ownership: 'shared', reason: null },
+ skills: { status: 'supported', ownership: 'shared', reason: null },
+ apiKeys: { status: 'supported', ownership: 'shared', reason: null },
+ },
+ },
+ },
+ ],
+ },
+ cliStatusLoading: false,
+ }),
+ ).toContain('Anthropic plugins unavailable');
+ });
+
+ it('allows multimodel MCP actions without aggregate auth when MCP support is declared', () => {
+ expect(
+ getExtensionActionDisableReason({
+ isInstalled: false,
+ section: 'mcp',
+ cliStatus: {
+ installed: true,
+ authLoggedIn: false,
+ binaryPath: '/usr/local/bin/claude-multimodel',
+ launchError: null,
+ flavor: 'agent_teams_orchestrator',
+ providers: [
+ {
+ providerId: 'codex',
+ displayName: 'Codex',
+ supported: true,
+ authenticated: false,
+ authMethod: null,
+ verificationState: 'unknown',
+ models: [],
+ canLoginFromUi: true,
+ capabilities: {
+ teamLaunch: true,
+ oneShot: true,
+ extensions: {
+ plugins: { status: 'unsupported', ownership: 'shared', reason: null },
+ mcp: { status: 'supported', ownership: 'shared', reason: null },
+ skills: { status: 'supported', ownership: 'shared', reason: null },
+ apiKeys: { status: 'supported', ownership: 'shared', reason: null },
+ },
+ },
+ },
+ ],
+ },
+ cliStatusLoading: false,
+ }),
+ ).toBeNull();
+ });
});
describe('sanitizeMcpServerName', () => {