+ {providerKeyCards.length > 0 && (
+
+ {providerKeyCards.map((provider) => (
+
+
+
+
{provider.label}
+
{provider.envVar}
+
+
+ {provider.authenticated
+ ? 'Connected'
+ : provider.apiKeyConfigured
+ ? 'Key configured'
+ : 'Key missing'}
+
+
+
+ {provider.sourceLabel
+ ? `Current source: ${provider.sourceLabel}.`
+ : 'No stored or environment key detected for this provider.'}
+ {provider.statusMessage ? ` ${provider.statusMessage}` : ''}
+
+
+ ))}
+
+ )}
{/* Header row */}
@@ -138,7 +221,13 @@ export const ApiKeysPanel = (): React.JSX.Element => {
)}
{/* Form dialog */}
-
+
);
};
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/CustomMcpServerDialog.tsx b/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx
index 7cb8e740..727d2603 100644
--- a/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx
+++ b/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx
@@ -3,7 +3,7 @@
* Supports stdio (npm package) and HTTP/SSE transports.
*/
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { api } from '@renderer/api';
import { Button } from '@renderer/components/ui/button';
@@ -24,6 +24,13 @@ import {
SelectValue,
} from '@renderer/components/ui/select';
import { useStore } from '@renderer/store';
+import { getExtensionActionDisableReason } from '@shared/utils/extensionNormalizers';
+import {
+ getDefaultMcpSharedScope,
+ getMcpScopeLabel,
+ isProjectScopedMcpScope,
+ isSharedMcpScope,
+} from '@shared/utils/mcpScopes';
import { Plus, Server, Trash2 } from 'lucide-react';
import type {
@@ -42,13 +49,7 @@ interface CustomMcpServerDialogProps {
type TransportMode = 'stdio' | 'http';
type HttpTransport = 'streamable-http' | 'sse' | 'http';
-type Scope = 'local' | 'user' | 'project';
-
-const SCOPE_OPTIONS: { value: Scope; label: string }[] = [
- { value: 'user', label: 'User (global)' },
- { value: 'project', label: 'Project' },
- { value: 'local', label: 'Local' },
-];
+type Scope = 'local' | 'user' | 'project' | 'global';
const HTTP_TRANSPORT_OPTIONS: { value: HttpTransport; label: string }[] = [
{ value: 'streamable-http', label: 'Streamable HTTP' },
@@ -67,11 +68,19 @@ export const CustomMcpServerDialog = ({
projectPath,
}: CustomMcpServerDialogProps): React.JSX.Element => {
const installCustomMcpServer = useStore((s) => s.installCustomMcpServer);
+ const cliStatus = useStore((s) => s.cliStatus);
+ const cliStatusLoading = useStore((s) => s.cliStatusLoading);
+ const defaultSharedScope = getDefaultMcpSharedScope(cliStatus?.flavor);
+ const scopeOptions: { value: Scope; label: string }[] = [
+ { value: defaultSharedScope, label: getMcpScopeLabel(defaultSharedScope, cliStatus?.flavor) },
+ { value: 'project', label: 'Project' },
+ { value: 'local', label: 'Local' },
+ ];
// Form state
const [serverName, setServerName] = useState('');
const [transportMode, setTransportMode] = useState
('stdio');
- const [scope, setScope] = useState('user');
+ const [scope, setScope] = useState(defaultSharedScope);
// Stdio fields
const [npmPackage, setNpmPackage] = useState('');
@@ -86,13 +95,31 @@ export const CustomMcpServerDialog = ({
const [envVars, setEnvVars] = useState([]);
const [error, setError] = useState(null);
const [installing, setInstalling] = useState(false);
+ const autoFilledValuesRef = useRef>({});
+ const wasOpenRef = useRef(false);
+ const previousDefaultSharedScopeRef = useRef(defaultSharedScope);
+ const envVarLookupNames = envVars
+ .map((entry) => entry.key.trim())
+ .filter(Boolean)
+ .sort()
+ .join('\0');
+ const apiKeyLookupProjectPath = isProjectScopedMcpScope(scope)
+ ? (projectPath ?? undefined)
+ : undefined;
+ const mutationDisableReason = getExtensionActionDisableReason({
+ isInstalled: false,
+ cliStatus,
+ cliStatusLoading,
+ section: 'mcp',
+ });
// Reset on open
useEffect(() => {
- if (open) {
+ const justOpened = open && !wasOpenRef.current;
+ if (justOpened) {
setServerName('');
setTransportMode('stdio');
- setScope('user');
+ setScope(defaultSharedScope);
setNpmPackage('');
setNpmVersion('');
setHttpUrl('');
@@ -101,39 +128,98 @@ export const CustomMcpServerDialog = ({
setEnvVars([]);
setError(null);
setInstalling(false);
+ autoFilledValuesRef.current = {};
}
- }, [open]);
+ wasOpenRef.current = open;
+ if (!open) {
+ previousDefaultSharedScopeRef.current = defaultSharedScope;
+ }
+ }, [defaultSharedScope, open]);
useEffect(() => {
- if (open && scope !== 'user' && !projectPath) {
- setScope('user');
+ if (!open) {
+ previousDefaultSharedScopeRef.current = defaultSharedScope;
+ return;
}
- }, [open, projectPath, scope]);
+
+ const previousDefaultSharedScope = previousDefaultSharedScopeRef.current;
+ if (
+ previousDefaultSharedScope !== defaultSharedScope &&
+ scope === previousDefaultSharedScope &&
+ isSharedMcpScope(scope)
+ ) {
+ setScope(defaultSharedScope);
+ }
+
+ previousDefaultSharedScopeRef.current = defaultSharedScope;
+ }, [defaultSharedScope, open, scope]);
+
+ useEffect(() => {
+ if (open && isProjectScopedMcpScope(scope) && !projectPath) {
+ setScope(defaultSharedScope);
+ }
+ }, [defaultSharedScope, open, projectPath, scope]);
// Auto-fill env vars from saved API keys
useEffect(() => {
if (!open || envVars.length === 0 || !api.apiKeys) return;
- const envVarNames = envVars.map((e) => e.key).filter(Boolean);
+ const envVarNames = envVars.map((e) => e.key.trim()).filter(Boolean);
if (envVarNames.length === 0) return;
- void api.apiKeys.lookup(envVarNames).then(
+ void api.apiKeys.lookup(envVarNames, apiKeyLookupProjectPath).then(
(results) => {
- if (results.length === 0) return;
- const lookup = new Map(results.map((r) => [r.envVarName, r.value]));
- setEnvVars((prev) =>
- prev.map((e) => (lookup.has(e.key) && !e.value ? { ...e, value: lookup.get(e.key)! } : e))
+ const previousAutoFilledValues = autoFilledValuesRef.current;
+ const nextAutoFilledValues = Object.fromEntries(
+ results.map((result) => [result.envVarName, result.value])
);
+ setEnvVars((prev) => {
+ let changed = false;
+ const next = prev.map((entry) => {
+ const envVarName = entry.key.trim();
+ if (!envVarName) {
+ return entry;
+ }
+
+ const previousValue = previousAutoFilledValues[envVarName];
+ const nextValue = nextAutoFilledValues[envVarName];
+
+ if (!nextValue) {
+ if (previousValue && entry.value === previousValue) {
+ changed = true;
+ return { ...entry, value: '' };
+ }
+ return entry;
+ }
+
+ if (!entry.value || entry.value === previousValue) {
+ if (entry.value !== nextValue) {
+ changed = true;
+ return { ...entry, value: nextValue };
+ }
+ }
+
+ return entry;
+ });
+
+ return changed ? next : prev;
+ });
+ autoFilledValuesRef.current = nextAutoFilledValues;
},
() => {
// Silently fail
}
);
- }, [open, envVars.length]); // eslint-disable-line react-hooks/exhaustive-deps
+ }, [apiKeyLookupProjectPath, envVarLookupNames, open]); // eslint-disable-line react-hooks/exhaustive-deps
const handleInstall = async () => {
setError(null);
+ if (mutationDisableReason) {
+ setError(mutationDisableReason);
+ return;
+ }
+
if (!serverName.trim()) {
setError('Server name is required');
return;
@@ -177,7 +263,7 @@ export const CustomMcpServerDialog = ({
const request: McpCustomInstallRequest = {
serverName,
scope,
- projectPath: scope !== 'user' ? (projectPath ?? undefined) : undefined,
+ projectPath: isProjectScopedMcpScope(scope) ? (projectPath ?? undefined) : undefined,
installSpec,
envValues,
headers: headers.filter((h) => h.key.trim() && h.value.trim()),
@@ -207,7 +293,8 @@ export const CustomMcpServerDialog = ({
const canSubmit =
serverName.trim() &&
(transportMode === 'stdio' ? npmPackage.trim() : httpUrl.trim()) &&
- !(scope !== 'user' && !projectPath) &&
+ !(isProjectScopedMcpScope(scope) && !projectPath) &&
+ !mutationDisableReason &&
!installing;
return (
@@ -382,11 +469,11 @@ export const CustomMcpServerDialog = ({
- {SCOPE_OPTIONS.map((opt) => (
+ {scopeOptions.map((opt) => (
{opt.label}
@@ -436,6 +523,11 @@ export const CustomMcpServerDialog = ({
{/* Error */}
+ {mutationDisableReason && (
+
{error}
diff --git a/src/renderer/components/extensions/mcp/McpServerCard.tsx b/src/renderer/components/extensions/mcp/McpServerCard.tsx
index afae2142..10844f74 100644
--- a/src/renderer/components/extensions/mcp/McpServerCard.tsx
+++ b/src/renderer/components/extensions/mcp/McpServerCard.tsx
@@ -16,6 +16,7 @@ import {
getMcpOperationKey,
sanitizeMcpServerName,
} from '@shared/utils/extensionNormalizers';
+import { getDefaultMcpSharedScope } from '@shared/utils/mcpScopes';
import { Clock, Cloud, Globe, KeyRound, Lock, Monitor, Star, Tag, Wrench } from 'lucide-react';
import { Github as GithubIcon } from 'lucide-react';
@@ -47,7 +48,9 @@ export const McpServerCard = ({
diagnosticsLoading,
onClick,
}: McpServerCardProps): React.JSX.Element => {
- const operationKey = getMcpOperationKey(server.id, 'user');
+ const cliStatus = useStore((s) => s.cliStatus);
+ const sharedScope = getDefaultMcpSharedScope(cliStatus?.flavor);
+ const operationKey = getMcpOperationKey(server.id, sharedScope);
const installProgress = useStore((s) => s.mcpInstallProgress[operationKey] ?? 'idle');
const installMcpServer = useStore((s) => s.installMcpServer);
const uninstallMcpServer = useStore((s) => s.uninstallMcpServer);
@@ -67,13 +70,13 @@ export const McpServerCard = ({
server.requiresAuth ||
(server.authHeaders?.length ?? 0) > 0;
const defaultServerName = sanitizeMcpServerName(server.name);
- const userInstallEntry =
- normalizedInstalledEntries.find((entry) => entry.scope === 'user') ?? null;
+ const sharedInstallEntry =
+ normalizedInstalledEntries.find((entry) => entry.scope === sharedScope) ?? null;
const installSummaryLabel = getMcpInstallationSummaryLabel(normalizedInstalledEntries);
const supportsDirectInstalledAction =
isInstalled &&
normalizedInstalledEntries.length === 1 &&
- userInstallEntry?.name === defaultServerName &&
+ sharedInstallEntry?.name === defaultServerName &&
!requiresConfiguration;
const shouldShowDirectInstallButton =
canAutoInstall && (!isInstalled ? !requiresConfiguration : supportsDirectInstalledAction);
@@ -258,17 +261,22 @@ export const McpServerCard = ({
installMcpServer({
registryId: server.id,
serverName: defaultServerName,
- scope: 'user',
+ scope: sharedScope,
envValues: {},
headers: [],
})
}
onUninstall={() =>
- uninstallMcpServer(server.id, userInstallEntry?.name ?? defaultServerName, 'user')
+ uninstallMcpServer(
+ server.id,
+ sharedInstallEntry?.name ?? defaultServerName,
+ sharedScope
+ )
}
size="sm"
errorMessage={installError}
diff --git a/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx b/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx
index d16e0885..97845c01 100644
--- a/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx
+++ b/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx
@@ -3,7 +3,7 @@
* Uses Radix UI Kit for all form elements.
*/
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
import { api } from '@renderer/api';
import { Badge } from '@renderer/components/ui/badge';
@@ -31,6 +31,12 @@ import {
getPreferredMcpInstallationEntry,
sanitizeMcpServerName,
} from '@shared/utils/extensionNormalizers';
+import {
+ getDefaultMcpSharedScope,
+ getMcpScopeLabel,
+ isProjectScopedMcpScope,
+ isSharedMcpScope,
+} from '@shared/utils/mcpScopes';
import { ExternalLink, Lock, Plus, Star, Trash2, Wrench } from 'lucide-react';
import { InstallButton } from '../common/InstallButton';
@@ -55,13 +61,7 @@ interface McpServerDetailDialogProps {
onClose: () => void;
}
-type Scope = 'local' | 'user' | 'project';
-
-const SCOPE_OPTIONS: { value: Scope; label: string }[] = [
- { value: 'user', label: 'User (global)' },
- { value: 'project', label: 'Project' },
- { value: 'local', label: 'Local' },
-];
+type Scope = 'local' | 'user' | 'project' | 'global';
export const McpServerDetailDialog = ({
server,
@@ -74,8 +74,10 @@ export const McpServerDetailDialog = ({
open,
onClose,
}: McpServerDetailDialogProps): React.JSX.Element => {
- const [scope, setScope] = useState('user');
- const operationKey = server ? getMcpOperationKey(server.id, scope) : null;
+ const cliStatus = useStore((s) => s.cliStatus);
+ const defaultSharedScope = getDefaultMcpSharedScope(cliStatus?.flavor);
+ const [scope, setScope] = useState(defaultSharedScope);
+ const operationKey = server ? getMcpOperationKey(server.id, scope, projectPath) : null;
const installProgress = useStore(
(s) => (operationKey ? s.mcpInstallProgress[operationKey] : undefined) ?? 'idle'
);
@@ -91,15 +93,36 @@ export const McpServerDetailDialog = ({
const [headers, setHeaders] = useState([]);
const [imgError, setImgError] = useState(false);
const [autoFilledFields, setAutoFilledFields] = useState>(new Set());
+ const autoFilledValuesRef = useRef>({});
+ const previousDefaultSharedScopeRef = useRef(defaultSharedScope);
const normalizedInstalledEntries = installedEntries.length
? installedEntries
: installedEntry
? [installedEntry]
: [];
+ const scopeOptions: { value: Scope; label: string }[] = [
+ { value: defaultSharedScope, label: getMcpScopeLabel(defaultSharedScope, cliStatus?.flavor) },
+ ...(defaultSharedScope !== 'user' &&
+ normalizedInstalledEntries.some((entry) => entry.scope === 'user')
+ ? [{ value: 'user' as const, label: getMcpScopeLabel('user', cliStatus?.flavor) }]
+ : []),
+ { value: 'project', label: 'Project' },
+ { value: 'local', label: 'Local' },
+ ];
const preferredInstalledEntry = getPreferredMcpInstallationEntry(normalizedInstalledEntries);
const selectedInstalledEntry =
normalizedInstalledEntries.find((entry) => entry.scope === scope) ?? null;
const installSummaryLabel = getMcpInstallationSummaryLabel(normalizedInstalledEntries);
+ const envVarLookupNames =
+ server?.envVars
+ .map((entry) => entry.name)
+ .sort()
+ .join('\0') ?? '';
+ const statusSectionLabel =
+ cliStatus?.flavor === 'agent_teams_orchestrator' ? 'Runtime Status' : 'Claude Status';
+ const apiKeyLookupProjectPath = isProjectScopedMcpScope(scope)
+ ? (projectPath ?? undefined)
+ : undefined;
// Initialize form when dialog opens or server changes
useEffect(() => {
@@ -120,47 +143,82 @@ export const McpServerDetailDialog = ({
}))
);
setServerName(preferredInstalledEntry?.name ?? sanitizeMcpServerName(server.name));
- setScope(preferredInstalledEntry?.scope ?? 'user');
+ setScope((preferredInstalledEntry?.scope as Scope | undefined) ?? defaultSharedScope);
setImgError(false);
setAutoFilledFields(new Set());
+ autoFilledValuesRef.current = {};
}, [open, preferredInstalledEntry?.name, preferredInstalledEntry?.scope, server?.id]);
useEffect(() => {
- if (!server || !open) {
+ if (!open) {
+ previousDefaultSharedScopeRef.current = defaultSharedScope;
return;
}
- setServerName(selectedInstalledEntry?.name ?? sanitizeMcpServerName(server.name));
- }, [open, scope, selectedInstalledEntry?.name, server]);
+ const previousDefaultSharedScope = previousDefaultSharedScopeRef.current;
+ if (
+ previousDefaultSharedScope !== defaultSharedScope &&
+ !preferredInstalledEntry &&
+ scope === previousDefaultSharedScope &&
+ isSharedMcpScope(scope)
+ ) {
+ setScope(defaultSharedScope);
+ }
+
+ previousDefaultSharedScopeRef.current = defaultSharedScope;
+ }, [defaultSharedScope, open, preferredInstalledEntry, scope]);
useEffect(() => {
- if (open && scope !== 'user' && !projectPath) {
- setScope('user');
+ if (!server || !open || !selectedInstalledEntry) {
+ return;
}
- }, [open, projectPath, scope]);
+
+ setServerName(selectedInstalledEntry.name);
+ }, [open, selectedInstalledEntry, server]);
+
+ useEffect(() => {
+ if (open && isProjectScopedMcpScope(scope) && !projectPath) {
+ setScope(defaultSharedScope);
+ }
+ }, [defaultSharedScope, open, projectPath, scope]);
// Auto-fill env values from saved API keys
useEffect(() => {
if (!server || !open || server.envVars.length === 0 || !api.apiKeys) return;
const envVarNames = server.envVars.map((e) => e.name);
- void api.apiKeys.lookup(envVarNames).then(
+ void api.apiKeys.lookup(envVarNames, apiKeyLookupProjectPath).then(
(results) => {
- if (results.length === 0) return;
- const filled = new Set();
- const values: Record = {};
+ const previousAutoFilledValues = autoFilledValuesRef.current;
+ const nextAutoFilledValues: Record = {};
for (const r of results) {
- values[r.envVarName] = r.value;
- filled.add(r.envVarName);
+ nextAutoFilledValues[r.envVarName] = r.value;
}
- setEnvValues((prev) => ({ ...prev, ...values }));
- setAutoFilledFields(filled);
+ setEnvValues((prev) => {
+ const next = { ...prev };
+
+ for (const [envVarName, previousValue] of Object.entries(previousAutoFilledValues)) {
+ if (!(envVarName in nextAutoFilledValues) && next[envVarName] === previousValue) {
+ next[envVarName] = '';
+ }
+ }
+
+ for (const [envVarName, nextValue] of Object.entries(nextAutoFilledValues)) {
+ if (!next[envVarName] || next[envVarName] === previousAutoFilledValues[envVarName]) {
+ next[envVarName] = nextValue;
+ }
+ }
+
+ return next;
+ });
+ setAutoFilledFields(new Set(Object.keys(nextAutoFilledValues)));
+ autoFilledValuesRef.current = nextAutoFilledValues;
},
() => {
// Silently fail — auto-fill is supplementary
}
);
- }, [server?.id, open]); // eslint-disable-line react-hooks/exhaustive-deps
+ }, [apiKeyLookupProjectPath, envVarLookupNames, open, server?.id]); // eslint-disable-line react-hooks/exhaustive-deps
if (!server) return <>>;
@@ -181,7 +239,7 @@ export const McpServerDetailDialog = ({
const isInstalledForScope = selectedInstalledEntry !== null;
const uninstallServerName = selectedInstalledEntry?.name ?? serverName;
const uninstallScope = selectedInstalledEntry?.scope ?? scope;
- const scopeRequiresProjectPath = scope !== 'user' && !projectPath;
+ const scopeRequiresProjectPath = isProjectScopedMcpScope(scope) && !projectPath;
const installDisabled =
!serverName.trim() ||
missingRequiredEnvVars ||
@@ -201,7 +259,7 @@ export const McpServerDetailDialog = ({
registryId: server.id,
serverName,
scope,
- projectPath: scope !== 'user' ? (projectPath ?? undefined) : undefined,
+ projectPath: isProjectScopedMcpScope(scope) ? (projectPath ?? undefined) : undefined,
envValues,
headers,
});
@@ -212,7 +270,7 @@ export const McpServerDetailDialog = ({
server.id,
uninstallServerName,
uninstallScope,
- uninstallScope !== 'user' ? (projectPath ?? undefined) : undefined
+ isProjectScopedMcpScope(uninstallScope) ? (projectPath ?? undefined) : undefined
);
};
@@ -353,7 +411,7 @@ export const McpServerDetailDialog = ({
{isInstalledForScope && (
-
Claude Status
+
{statusSectionLabel}
{diagnosticsLoading && !diagnostic ? (
- {SCOPE_OPTIONS.map((opt) => (
+ {scopeOptions.map((opt) => (
{opt.label}
@@ -528,6 +586,7 @@ export const McpServerDetailDialog = ({
{
+ const projectStateKey = getMcpProjectStateKey(projectPath);
const {
browseCatalog,
browseNextCursor,
browseLoading,
browseError,
mcpBrowse,
- installedServers,
+ installedServersByProjectPath,
+ installedServersFallback,
fetchMcpGitHubStars,
- mcpDiagnostics,
- mcpDiagnosticsLoading,
- mcpDiagnosticsError,
- mcpDiagnosticsLastCheckedAt,
+ mcpDiagnosticsByProjectPath,
+ mcpDiagnosticsFallback,
+ mcpDiagnosticsLoadingByProjectPath,
+ mcpDiagnosticsLoadingFallback,
+ mcpDiagnosticsErrorByProjectPath,
+ mcpDiagnosticsErrorFallback,
+ mcpDiagnosticsLastCheckedAtByProjectPath,
+ mcpDiagnosticsLastCheckedAtFallback,
runMcpDiagnostics,
+ cliStatus,
+ cliStatusLoading,
} = useStore(
useShallow((s) => ({
browseCatalog: s.mcpBrowseCatalog,
@@ -98,15 +108,34 @@ export const McpServersPanel = ({
browseLoading: s.mcpBrowseLoading,
browseError: s.mcpBrowseError,
mcpBrowse: s.mcpBrowse,
- installedServers: s.mcpInstalledServers,
+ installedServersByProjectPath: s.mcpInstalledServersByProjectPath,
+ installedServersFallback: s.mcpInstalledServers,
fetchMcpGitHubStars: s.fetchMcpGitHubStars,
- mcpDiagnostics: s.mcpDiagnostics,
- mcpDiagnosticsLoading: s.mcpDiagnosticsLoading,
- mcpDiagnosticsError: s.mcpDiagnosticsError,
- mcpDiagnosticsLastCheckedAt: s.mcpDiagnosticsLastCheckedAt,
+ mcpDiagnosticsByProjectPath: s.mcpDiagnosticsByProjectPath,
+ mcpDiagnosticsFallback: s.mcpDiagnostics,
+ mcpDiagnosticsLoadingByProjectPath: s.mcpDiagnosticsLoadingByProjectPath,
+ mcpDiagnosticsLoadingFallback: s.mcpDiagnosticsLoading,
+ mcpDiagnosticsErrorByProjectPath: s.mcpDiagnosticsErrorByProjectPath,
+ mcpDiagnosticsErrorFallback: s.mcpDiagnosticsError,
+ mcpDiagnosticsLastCheckedAtByProjectPath: s.mcpDiagnosticsLastCheckedAtByProjectPath,
+ mcpDiagnosticsLastCheckedAtFallback: s.mcpDiagnosticsLastCheckedAt,
runMcpDiagnostics: s.runMcpDiagnostics,
+ cliStatus: s.cliStatus,
+ cliStatusLoading: s.cliStatusLoading,
}))
);
+ const installedServers =
+ installedServersByProjectPath?.[projectStateKey] ?? installedServersFallback ?? [];
+ const mcpDiagnostics =
+ mcpDiagnosticsByProjectPath?.[projectStateKey] ?? mcpDiagnosticsFallback ?? {};
+ const mcpDiagnosticsLoading =
+ mcpDiagnosticsLoadingByProjectPath?.[projectStateKey] ?? mcpDiagnosticsLoadingFallback ?? false;
+ const mcpDiagnosticsError =
+ mcpDiagnosticsErrorByProjectPath?.[projectStateKey] ?? mcpDiagnosticsErrorFallback ?? null;
+ const mcpDiagnosticsLastCheckedAt =
+ mcpDiagnosticsLastCheckedAtByProjectPath?.[projectStateKey] ??
+ mcpDiagnosticsLastCheckedAtFallback ??
+ null;
const [mcpSort, setMcpSort] = useState('name-asc');
@@ -117,9 +146,31 @@ export const McpServersPanel = ({
}
}, [browseCatalog.length, browseError, browseLoading, mcpBrowse]);
+ const diagnosticsDisableReason = useMemo(() => {
+ if (cliStatusLoading) {
+ return 'Checking runtime status...';
+ }
+
+ if (cliStatus === null || typeof cliStatus === 'undefined') {
+ return 'Checking runtime availability...';
+ }
+
+ if (cliStatus?.installed === false) {
+ if (cliStatus.binaryPath && cliStatus.launchError) {
+ return 'The configured runtime was found but failed to start. Open the Dashboard to repair or reinstall it.';
+ }
+ return 'The configured runtime is required. Install or repair it from the Dashboard.';
+ }
+
+ return null;
+ }, [cliStatus, cliStatusLoading]);
+
useEffect(() => {
- void runMcpDiagnostics();
- }, [runMcpDiagnostics]);
+ if (diagnosticsDisableReason) {
+ return;
+ }
+ void runMcpDiagnostics(projectPath ?? undefined);
+ }, [diagnosticsDisableReason, projectPath, runMcpDiagnostics]);
// Fetch GitHub stars after catalog loads (fire-and-forget)
useEffect(() => {
@@ -162,7 +213,12 @@ export const McpServersPanel = ({
const getDiagnostic = (server: McpCatalogItem): McpServerDiagnostic | null => {
const installedEntry = getInstalledEntry(server);
- return installedEntry ? (mcpDiagnostics[installedEntry.name] ?? null) : null;
+ return installedEntry
+ ? (mcpDiagnostics[getMcpDiagnosticKey(installedEntry.name, installedEntry.scope)] ??
+ mcpDiagnostics[getMcpDiagnosticKey(installedEntry.name)] ??
+ mcpDiagnostics[installedEntry.name] ??
+ null)
+ : null;
};
const allDiagnostics = useMemo(
@@ -185,6 +241,8 @@ 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';
// Find selected server (search in both lists to avoid losing selection during search toggle)
const selectedServer = useMemo(() => {
@@ -205,24 +263,21 @@ export const McpServersPanel = ({
MCP Health Status
{mcpDiagnosticsLoading ? (
- <>
- Checking installed MCP servers via Claude CLI (claude mcp list) ...
- >
+ <>Checking installed MCP servers via {runtimeLabel} ...>
+ ) : diagnosticsDisableReason ? (
+ diagnosticsDisableReason
) : mcpDiagnosticsLastCheckedAt ? (
`Last checked ${formatRelativeTime(new Date(mcpDiagnosticsLastCheckedAt).toISOString())}`
) : (
- <>
- Run diagnostics (claude mcp list) to verify installed MCP
- connectivity.
- >
+ <>Run diagnostics from this page to verify installed MCP connectivity.>
)}
void runMcpDiagnostics()}
- disabled={mcpDiagnosticsLoading}
+ onClick={() => void runMcpDiagnostics(projectPath ?? undefined)}
+ disabled={mcpDiagnosticsLoading || Boolean(diagnosticsDisableReason)}
className="whitespace-nowrap"
>
0) && (
-
Claude MCP List Results
+
Runtime MCP Diagnostics
{allDiagnostics.length > 0 && (
{allDiagnostics.length} servers
)}
@@ -244,11 +299,18 @@ export const McpServersPanel = ({
{allDiagnostics.map((diagnostic) => (
-
{diagnostic.name}
+
+
{diagnostic.name}
+ {diagnostic.scope && (
+
+ {diagnostic.scope}
+
+ )}
+
) : (
-
Waiting for `claude mcp list` results...
+
Waiting for diagnostics results...
)}
)}
@@ -347,10 +409,15 @@ export const McpServersPanel = ({
-
Claude CLI not installed
+
+ {cliStatus?.flavor === 'agent_teams_orchestrator'
+ ? 'Configured runtime not available'
+ : 'Claude CLI not installed'}
+
- MCP health checks require Claude CLI. Go to the Dashboard to install it
- automatically.
+ {cliStatus?.flavor === 'agent_teams_orchestrator'
+ ? 'MCP health checks require the configured runtime. Go to the Dashboard to install or repair it.'
+ : 'MCP health checks require Claude CLI. Go to the Dashboard to install or repair it.'}
diff --git a/src/renderer/components/extensions/plugins/PluginCard.tsx b/src/renderer/components/extensions/plugins/PluginCard.tsx
index 1b7818bf..0f7230e1 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 46295c2b..5b4a4274 100644
--- a/src/renderer/components/extensions/plugins/PluginDetailDialog.tsx
+++ b/src/renderer/components/extensions/plugins/PluginDetailDialog.tsx
@@ -91,7 +91,9 @@ export const PluginDetailDialog = ({
}
}, [projectScopeAvailable, scope]);
- const operationKey = plugin ? getPluginOperationKey(plugin.pluginId, scope) : null;
+ const operationKey = plugin
+ ? getPluginOperationKey(plugin.pluginId, scope, scope !== 'user' ? projectPath : undefined)
+ : null;
const installProgress = useStore(
(s) => (operationKey ? s.pluginInstallProgress[operationKey] : undefined) ?? 'idle'
);
@@ -195,6 +197,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..8adde28f 100644
--- a/src/renderer/components/extensions/plugins/PluginsPanel.tsx
+++ b/src/renderer/components/extensions/plugins/PluginsPanel.tsx
@@ -17,6 +17,7 @@ import {
} from '@renderer/components/ui/select';
import { useStore } from '@renderer/store';
import { inferCapabilities, normalizeCategory } from '@shared/utils/extensionNormalizers';
+import { getCliProviderExtensionCapability } from '@shared/utils/providerExtensionCapabilities';
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,27 @@ 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 (
+
+ In the multimodel runtime, plugins currently apply only to Anthropic sessions. Broader
+ plugin support across providers is in development.
+ {capability.reason ? ` ${capability.reason}` : ''}
+
+ );
+ })()}
{/* Search + Sort + Installed only row */}
diff --git a/src/renderer/components/extensions/skills/SkillDetailDialog.tsx b/src/renderer/components/extensions/skills/SkillDetailDialog.tsx
index 2b57a9ca..458bcce7 100644
--- a/src/renderer/components/extensions/skills/SkillDetailDialog.tsx
+++ b/src/renderer/components/extensions/skills/SkillDetailDialog.tsx
@@ -23,11 +23,14 @@ import {
DialogTitle,
} from '@renderer/components/ui/dialog';
import { useStore } from '@renderer/store';
-import { AlertTriangle, ExternalLink, FolderOpen, Pencil, Trash2 } from 'lucide-react';
+import { formatSkillRootKind, getSkillAudienceLabel } from '@shared/utils/skillRoots';
+import { AlertTriangle, ExternalLink, FolderOpen, Info, Pencil, Trash2 } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow';
import { resolveSkillProjectPath } from './skillProjectUtils';
+import type { SkillValidationIssue } from '@shared/types';
+
interface SkillDetailDialogProps {
skillId: string | null;
open: boolean;
@@ -80,10 +83,7 @@ export const SkillDetailDialog = ({
const effectiveProjectPath = item
? resolveSkillProjectPath(item.scope, projectPath, item.projectRoot)
: (projectPath ?? undefined);
-
- function formatRootKind(rootKind: 'claude' | 'cursor' | 'agents'): string {
- return `.${rootKind}`;
- }
+ const issuesTone = item?.issues.length ? getIssuesTone(item.issues) : null;
function formatScopeLabel(scope: 'user' | 'project'): string {
return scope === 'project' ? 'This project only' : 'Your personal skills';
@@ -91,8 +91,29 @@ export const SkillDetailDialog = ({
function formatInvocationLabel(invocationMode: 'auto' | 'manual-only'): string {
return invocationMode === 'manual-only'
- ? 'Claude will only use this when you explicitly ask for it.'
- : 'Claude can pick this automatically when it matches the task.';
+ ? 'Only runs when you explicitly ask for it.'
+ : 'Runs automatically when it matches the task.';
+ }
+
+ function getIssuesTone(issues: SkillValidationIssue[]): {
+ className: string;
+ title: string;
+ Icon: typeof AlertTriangle;
+ } {
+ const informationalOnly = issues.every((issue) => issue.severity === 'info');
+ if (informationalOnly) {
+ return {
+ className: 'border-blue-500/30 bg-blue-500/5',
+ title: 'This skill includes bundled scripts',
+ Icon: Info,
+ };
+ }
+
+ return {
+ className: 'border-amber-500/30 bg-amber-500/5',
+ title: 'Review this skill carefully before using it',
+ Icon: AlertTriangle,
+ };
}
async function handleDelete(): Promise
{
@@ -159,7 +180,8 @@ export const SkillDetailDialog = ({
)}
{formatScopeLabel(item.scope)}
- Stored in {formatRootKind(item.rootKind)}
+ Stored in {formatSkillRootKind(item.rootKind)}
+ {getSkillAudienceLabel(item.rootKind)}
{item.invocationMode === 'manual-only' ? 'Manual use' : 'Auto use'}
@@ -169,16 +191,30 @@ export const SkillDetailDialog = ({
{item.issues.length > 0 && (
-
-
- Review this skill carefully before using it
+
+
+ {issuesTone?.title}
{item.issues.map((issue, index) => (
-
+ {issue.severity === 'info' ? (
+
+ ) : (
+
+ )}
{issue.message}
))}
@@ -194,7 +230,7 @@ export const SkillDetailDialog = ({
- How Claude uses it
+ How it is used
{formatInvocationLabel(item.invocationMode)}
diff --git a/src/renderer/components/extensions/skills/SkillEditorDialog.tsx b/src/renderer/components/extensions/skills/SkillEditorDialog.tsx
index f5e625bf..65885733 100644
--- a/src/renderer/components/extensions/skills/SkillEditorDialog.tsx
+++ b/src/renderer/components/extensions/skills/SkillEditorDialog.tsx
@@ -23,6 +23,7 @@ import {
import { Textarea } from '@renderer/components/ui/textarea';
import { useMarkdownScrollSync } from '@renderer/hooks/useMarkdownScrollSync';
import { useStore } from '@renderer/store';
+import { SKILL_ROOT_DEFINITIONS } from '@shared/utils/skillRoots';
import { FileSearch, RotateCcw, X } from 'lucide-react';
import { SkillCodeEditor } from './SkillCodeEditor';
@@ -41,6 +42,7 @@ import type {
SkillDetail,
SkillInvocationMode,
SkillReviewPreview,
+ SkillRootKind,
} from '@shared/types/extensions';
type EditorMode = 'create' | 'edit';
@@ -50,6 +52,7 @@ interface SkillEditorDialogProps {
mode: EditorMode;
projectPath: string | null;
projectLabel: string | null;
+ allowCodexRootKind: boolean;
detail: SkillDetail | null;
onClose: () => void;
onSaved: (skillId: string | null) => void;
@@ -68,6 +71,7 @@ export const SkillEditorDialog = ({
mode,
projectPath,
projectLabel,
+ allowCodexRootKind,
detail,
onClose,
onSaved,
@@ -79,7 +83,7 @@ export const SkillEditorDialog = ({
const applySkillUpsert = useStore((s) => s.applySkillUpsert);
const [scope, setScope] = useState<'user' | 'project'>('user');
- const [rootKind, setRootKind] = useState<'claude' | 'cursor' | 'agents'>('claude');
+ const [rootKind, setRootKind] = useState
('claude');
const [folderName, setFolderName] = useState('');
const [name, setName] = useState('');
const [description, setDescription] = useState('');
@@ -218,7 +222,7 @@ export const SkillEditorDialog = ({
setReviewLoading(false);
setSaveLoading(false);
setMutationError(null);
- }, [detail, mode, open, projectPath]);
+ }, [allowCodexRootKind, detail, mode, open, projectPath]);
useEffect(() => {
if (open) {
@@ -238,6 +242,12 @@ export const SkillEditorDialog = ({
}
}, [mode, open, projectPath, scope]);
+ useEffect(() => {
+ if (open && mode === 'create' && rootKind === 'codex' && !allowCodexRootKind) {
+ setRootKind('claude');
+ }
+ }, [allowCodexRootKind, mode, open, rootKind]);
+
useEffect(() => {
rawContentRef.current = rawContent;
}, [rawContent]);
@@ -289,6 +299,14 @@ export const SkillEditorDialog = ({
);
const canUseProjectScope = Boolean(projectPath);
+ const visibleRootDefinitions = useMemo(
+ () =>
+ SKILL_ROOT_DEFINITIONS.filter(
+ (definition) =>
+ definition.rootKind !== 'codex' || allowCodexRootKind || detail?.item.rootKind === 'codex'
+ ),
+ [allowCodexRootKind, detail?.item.rootKind]
+ );
const instructionsLocked = manualRawEdit || customMarkdownDetected;
const title = mode === 'create' ? 'Create skill' : 'Edit skill';
const descriptionText =
@@ -427,18 +445,19 @@ export const SkillEditorDialog = ({
Where to store it
- setRootKind(value as 'claude' | 'cursor' | 'agents')
- }
+ onValueChange={(value) => setRootKind(value as SkillRootKind)}
disabled={mode === 'edit'}
>
- .claude
- .cursor
- .agents
+ {visibleRootDefinitions.map((definition) => (
+
+ {definition.directoryName}
+ {definition.audience === 'codex' ? ' - Codex only' : ' - Shared'}
+
+ ))}
@@ -463,7 +482,7 @@ export const SkillEditorDialog = ({
-
How Claude should use it
+
How it should be used
{
@@ -476,7 +495,7 @@ export const SkillEditorDialog = ({
- Claude can use it automatically
+ Can be used automatically
Only when you ask for it
@@ -556,7 +575,7 @@ export const SkillEditorDialog = ({
-
When Claude should reach for this
+
When to reach for this
diff --git a/src/renderer/components/extensions/skills/SkillsPanel.tsx b/src/renderer/components/extensions/skills/SkillsPanel.tsx
index faf7e193..9c95959d 100644
--- a/src/renderer/components/extensions/skills/SkillsPanel.tsx
+++ b/src/renderer/components/extensions/skills/SkillsPanel.tsx
@@ -6,6 +6,17 @@ import { Button } from '@renderer/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { useStore } from '@renderer/store';
+import { getVisibleMultimodelProviders } from '@renderer/utils/multimodelProviderVisibility';
+import {
+ getCliProviderExtensionCapability,
+ isCliExtensionCapabilityAvailable,
+} from '@shared/utils/providerExtensionCapabilities';
+import {
+ formatSkillRootKind,
+ getSkillAudience,
+ getSkillAudienceLabel,
+ isCodexSkillOverlayAvailable,
+} from '@shared/utils/skillRoots';
import {
AlertTriangle,
ArrowUpAZ,
@@ -15,6 +26,7 @@ import {
CheckCircle2,
Clock3,
Download,
+ Info,
Plus,
Search,
} from 'lucide-react';
@@ -28,12 +40,19 @@ import { SkillImportDialog } from './SkillImportDialog';
import { resolveSkillProjectPath } from './skillProjectUtils';
import type { SkillsSortState } from '@renderer/hooks/useExtensionsTabState';
-import type { SkillCatalogItem, SkillDetail } from '@shared/types/extensions';
+import type { SkillCatalogItem, SkillDetail, SkillValidationIssue } from '@shared/types/extensions';
const SUCCESS_BANNER_MS = 2500;
const NEW_SKILL_HIGHLIGHT_MS = 4000;
const USER_SKILLS_CATALOG_KEY = '__user__';
-type SkillsQuickFilter = 'all' | 'project' | 'personal' | 'needs-attention' | 'has-scripts';
+type SkillsQuickFilter =
+ | 'all'
+ | 'project'
+ | 'personal'
+ | 'shared'
+ | 'codex-only'
+ | 'needs-attention'
+ | 'has-scripts';
interface SkillsPanelProps {
projectPath: string | null;
@@ -57,10 +76,6 @@ function sortSkills(skills: SkillCatalogItem[], sort: SkillsSortState): SkillCat
return next;
}
-function formatRootKind(rootKind: SkillCatalogItem['rootKind']): string {
- return `.${rootKind}`;
-}
-
function getScopeLabel(skill: SkillCatalogItem): string {
return skill.scope === 'project' ? 'This project' : 'Personal';
}
@@ -68,7 +83,7 @@ function getScopeLabel(skill: SkillCatalogItem): string {
function getInvocationLabel(skill: SkillCatalogItem): string {
return skill.invocationMode === 'manual-only'
? 'Only runs when you explicitly ask for it'
- : 'Claude can use this automatically when it fits';
+ : 'Runs automatically when it fits';
}
function getSkillStatus(skill: SkillCatalogItem): string {
@@ -81,6 +96,45 @@ function getSkillStatus(skill: SkillCatalogItem): string {
return 'Ready to use';
}
+function getPrimarySkillIssue(skill: SkillCatalogItem): SkillValidationIssue | null {
+ return (
+ skill.issues.find((issue) => issue.severity === 'error') ??
+ skill.issues.find((issue) => issue.severity === 'warning') ??
+ skill.issues[0] ??
+ null
+ );
+}
+
+function getSkillIssueTone(issue: SkillValidationIssue | null): {
+ className: string;
+ Icon: typeof AlertTriangle;
+} {
+ if (issue?.severity === 'info') {
+ return {
+ className: 'border-blue-500/20 bg-blue-500/10 text-blue-700 dark:text-blue-300',
+ Icon: Info,
+ };
+ }
+
+ return {
+ className: 'border-amber-500/20 bg-amber-500/5 text-amber-700 dark:text-amber-300',
+ Icon: AlertTriangle,
+ };
+}
+
+function formatRuntimeAudienceLabel(providerNames: readonly string[]): string {
+ if (providerNames.length === 0) {
+ return 'the configured runtime';
+ }
+ if (providerNames.length === 1) {
+ return providerNames[0];
+ }
+ if (providerNames.length === 2) {
+ return `${providerNames[0]} and ${providerNames[1]}`;
+ }
+ return `${providerNames.slice(0, -1).join(', ')}, and ${providerNames.at(-1)}`;
+}
+
export const SkillsPanel = ({
projectPath,
projectLabel,
@@ -94,6 +148,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));
@@ -110,18 +165,47 @@ export const SkillsPanel = ({
const [successMessage, setSuccessMessage] = useState
(null);
const [highlightedSkillId, setHighlightedSkillId] = useState(null);
const selectedSkillIdRef = useRef(selectedSkillId);
- const selectedSkillItemRef = useRef(null);
+ const selectedSkillItemRef = useRef(null);
selectedSkillIdRef.current = selectedSkillId;
const mergedSkills = useMemo(
() => [...projectSkills, ...userSkills],
[projectSkills, userSkills]
);
+ const codexSkillOverlayAvailable = useMemo(
+ () => isCodexSkillOverlayAvailable(cliStatus),
+ [cliStatus]
+ );
+ const skillsAudienceLabel = useMemo(() => {
+ if (cliStatus?.flavor !== 'agent_teams_orchestrator') {
+ return null;
+ }
+
+ const providerNames = getVisibleMultimodelProviders(cliStatus.providers ?? [])
+ .filter((provider) =>
+ isCliExtensionCapabilityAvailable(getCliProviderExtensionCapability(provider, 'skills'))
+ )
+ .map((provider) => provider.displayName);
+
+ return formatRuntimeAudienceLabel(providerNames);
+ }, [cliStatus]);
+ const codexOnlySkillsCount = useMemo(
+ () => mergedSkills.filter((skill) => getSkillAudience(skill.rootKind) === 'codex').length,
+ [mergedSkills]
+ );
+ const sharedSkillsCount = mergedSkills.length - codexOnlySkillsCount;
+ const showCodexOnlyUi = codexSkillOverlayAvailable || codexOnlySkillsCount > 0;
const selectedDetail = selectedSkillId ? (detailById[selectedSkillId] ?? null) : null;
selectedSkillItemRef.current = selectedSkillId
? (selectedDetail?.item ?? mergedSkills.find((skill) => skill.id === selectedSkillId) ?? null)
: null;
+ useEffect(() => {
+ if (quickFilter === 'codex-only' && !showCodexOnlyUi) {
+ setQuickFilter('all');
+ }
+ }, [quickFilter, showCodexOnlyUi]);
+
useEffect(() => {
if (!selectedSkillId) return;
if (mergedSkills.some((skill) => skill.id === selectedSkillId)) return;
@@ -204,6 +288,10 @@ export const SkillsPanel = ({
return skill.scope === 'project';
case 'personal':
return skill.scope === 'user';
+ case 'shared':
+ return getSkillAudience(skill.rootKind) === 'shared';
+ case 'codex-only':
+ return getSkillAudience(skill.rootKind) === 'codex';
case 'needs-attention':
return !skill.isValid;
case 'has-scripts':
@@ -226,16 +314,23 @@ export const SkillsPanel = ({
return (
+ {cliStatus?.flavor === 'agent_teams_orchestrator' && (
+
+ Shared skills in `.claude`, `.cursor`, and `.agents` are available to{' '}
+ {skillsAudienceLabel ?? 'the configured runtime'}. Skills stored in `.codex` stay
+ Codex-only when Codex support is available.
+
+ )}
-
Teach Claude repeatable work
+ Teach repeatable work
- Skills are reusable instructions that help Claude handle the same kind of task more
- consistently.{' '}
+ Skills are reusable instructions that help the runtime handle the same kind of task
+ more consistently.{' '}
{projectPath
? `You are seeing skills for ${projectLabel ?? projectPath} plus your personal skills.`
: 'You are seeing only your personal skills right now.'}
@@ -243,6 +338,9 @@ export const SkillsPanel = ({
Use personal skills for habits you want everywhere. Use project skills for workflows
that only make sense inside one codebase.
+ {codexSkillOverlayAvailable
+ ? ' Use `.codex` when a skill should stay Codex-only.'
+ : ' Existing `.codex` skills stay editable here, but new Codex-only skills need the Codex runtime enabled.'}
@@ -320,6 +418,14 @@ export const SkillsPanel = ({
{userSkills.length} personal
+
+ {sharedSkillsCount} shared
+
+ {showCodexOnlyUi && (
+
+ {codexOnlySkillsCount} Codex only
+
+ )}
@@ -331,6 +437,10 @@ export const SkillsPanel = ({
['all', 'All skills'],
['project', 'Project'],
['personal', 'Personal'],
+ ['shared', 'Shared'],
+ ...(showCodexOnlyUi
+ ? ([['codex-only', 'Codex only']] as [SkillsQuickFilter, string][])
+ : []),
['needs-attention', 'Needs attention'],
['has-scripts', 'Has scripts'],
] as [SkillsQuickFilter, string][]
@@ -383,7 +493,7 @@ export const SkillsPanel = ({
{skillsSearchQuery
? 'Try a different search term or switch filters.'
- : 'Create your first skill to teach Claude a repeatable workflow, or import one you already use.'}
+ : 'Create your first skill to teach a repeatable workflow, or import one you already use.'}
)}
@@ -404,71 +514,83 @@ export const SkillsPanel = ({
- {visibleProjectSkills.map((skill) => (
-
setSelectedSkillId(skill.id)}
- className={`rounded-xl border p-4 text-left transition-colors ${
- highlightedSkillId === skill.id
- ? 'border-green-500/50 bg-green-500/10 shadow-[0_0_0_1px_rgba(34,197,94,0.18)]'
- : 'bg-surface-raised/10 border-border hover:border-border-emphasis'
- }`}
- >
-
-
-
-
{skill.name}
- {!skill.isValid && (
-
- Needs attention
-
- )}
+ {visibleProjectSkills.map((skill) => {
+ const primaryIssue = getPrimarySkillIssue(skill);
+ const issueTone = getSkillIssueTone(primaryIssue);
+ const IssueIcon = issueTone.Icon;
+ return (
+
setSelectedSkillId(skill.id)}
+ className={`rounded-xl border p-4 text-left transition-colors ${
+ highlightedSkillId === skill.id
+ ? 'border-green-500/50 bg-green-500/10 shadow-[0_0_0_1px_rgba(34,197,94,0.18)]'
+ : 'bg-surface-raised/10 border-border hover:border-border-emphasis'
+ }`}
+ >
+
+
+
+
+ {skill.name}
+
+ {!skill.isValid && (
+
+ Needs attention
+
+ )}
+
+
+ {skill.description}
+
-
- {skill.description}
-
+
{getScopeLabel(skill)}
- {getScopeLabel(skill)}
-
-
-
{getInvocationLabel(skill)}
-
{getSkillStatus(skill)}
-
-
-
-
- Stored in {formatRootKind(skill.rootKind)}
-
- {skill.flags.hasScripts && (
-
- Has scripts
-
- )}
- {skill.flags.hasReferences && (
-
- References
-
- )}
- {skill.flags.hasAssets && (
-
- Assets
-
- )}
-
-
- {skill.issues.length > 0 && (
-
-
-
{skill.issues[0]?.message}
+
+
{getInvocationLabel(skill)}
+
{getSkillStatus(skill)}
- )}
-
- ))}
+
+
+
+ Stored in {formatSkillRootKind(skill.rootKind)}
+
+
+ {getSkillAudienceLabel(skill.rootKind)}
+
+ {skill.flags.hasScripts && (
+
+ Has scripts
+
+ )}
+ {skill.flags.hasReferences && (
+
+ References
+
+ )}
+ {skill.flags.hasAssets && (
+
+ Assets
+
+ )}
+
+
+ {primaryIssue && (
+
+
+ {primaryIssue.message}
+
+ )}
+
+ );
+ })}
)}
@@ -479,7 +601,7 @@ export const SkillsPanel = ({
Personal skills
- Habits and instructions you want Claude to remember everywhere.
+ Habits and instructions you want available everywhere.
@@ -487,71 +609,83 @@ export const SkillsPanel = ({
- {visibleUserSkills.map((skill) => (
-
setSelectedSkillId(skill.id)}
- className={`rounded-xl border p-4 text-left transition-colors ${
- highlightedSkillId === skill.id
- ? 'border-green-500/50 bg-green-500/10 shadow-[0_0_0_1px_rgba(34,197,94,0.18)]'
- : 'bg-surface-raised/10 border-border hover:border-border-emphasis'
- }`}
- >
-
-
-
-
{skill.name}
- {!skill.isValid && (
-
- Needs attention
-
- )}
+ {visibleUserSkills.map((skill) => {
+ const primaryIssue = getPrimarySkillIssue(skill);
+ const issueTone = getSkillIssueTone(primaryIssue);
+ const IssueIcon = issueTone.Icon;
+ return (
+
setSelectedSkillId(skill.id)}
+ className={`rounded-xl border p-4 text-left transition-colors ${
+ highlightedSkillId === skill.id
+ ? 'border-green-500/50 bg-green-500/10 shadow-[0_0_0_1px_rgba(34,197,94,0.18)]'
+ : 'bg-surface-raised/10 border-border hover:border-border-emphasis'
+ }`}
+ >
+
+
+
+
+ {skill.name}
+
+ {!skill.isValid && (
+
+ Needs attention
+
+ )}
+
+
+ {skill.description}
+
-
- {skill.description}
-
+
{getScopeLabel(skill)}
- {getScopeLabel(skill)}
-
-
-
{getInvocationLabel(skill)}
-
{getSkillStatus(skill)}
-
-
-
-
- Stored in {formatRootKind(skill.rootKind)}
-
- {skill.flags.hasScripts && (
-
- Has scripts
-
- )}
- {skill.flags.hasReferences && (
-
- References
-
- )}
- {skill.flags.hasAssets && (
-
- Assets
-
- )}
-
-
- {skill.issues.length > 0 && (
-
-
-
{skill.issues[0]?.message}
+
+
{getInvocationLabel(skill)}
+
{getSkillStatus(skill)}
- )}
-
- ))}
+
+
+
+ Stored in {formatSkillRootKind(skill.rootKind)}
+
+
+ {getSkillAudienceLabel(skill.rootKind)}
+
+ {skill.flags.hasScripts && (
+
+ Has scripts
+
+ )}
+ {skill.flags.hasReferences && (
+
+ References
+
+ )}
+ {skill.flags.hasAssets && (
+
+ Assets
+
+ )}
+
+
+ {primaryIssue && (
+
+
+ {primaryIssue.message}
+
+ )}
+
+ );
+ })}
)}
@@ -577,6 +711,7 @@ export const SkillsPanel = ({
mode="create"
projectPath={projectPath}
projectLabel={projectLabel}
+ allowCodexRootKind={codexSkillOverlayAvailable}
detail={null}
onClose={() => setCreateOpen(false)}
onSaved={(skillId) => {
@@ -592,6 +727,7 @@ export const SkillsPanel = ({
mode="edit"
projectPath={projectPath}
projectLabel={projectLabel}
+ allowCodexRootKind={codexSkillOverlayAvailable}
detail={editingDetail}
onClose={() => {
setEditOpen(false);
@@ -609,6 +745,7 @@ export const SkillsPanel = ({
open={importOpen}
projectPath={projectPath}
projectLabel={projectLabel}
+ allowCodexRootKind={codexSkillOverlayAvailable}
onClose={() => setImportOpen(false)}
onImported={(skillId) => {
setImportOpen(false);
diff --git a/src/renderer/components/layout/Sidebar.tsx b/src/renderer/components/layout/Sidebar.tsx
index 29918c71..b6f07b7a 100644
--- a/src/renderer/components/layout/Sidebar.tsx
+++ b/src/renderer/components/layout/Sidebar.tsx
@@ -204,7 +204,7 @@ export const Sidebar = (): React.JSX.Element => {
entry.envVarName === envVarName);
- return matches.find((entry) => entry.scope === 'user') ?? matches[0] ?? null;
+ return matches.find((entry) => entry.scope === 'user') ?? null;
}
function getConnectionDescription(provider: CliProviderStatus): string {
diff --git a/src/renderer/components/settings/hooks/useSettingsConfig.ts b/src/renderer/components/settings/hooks/useSettingsConfig.ts
index 0ab735d2..85ffa332 100644
--- a/src/renderer/components/settings/hooks/useSettingsConfig.ts
+++ b/src/renderer/components/settings/hooks/useSettingsConfig.ts
@@ -54,6 +54,7 @@ export interface SafeConfig {
notifyOnCrossTeamMessage: boolean;
notifyOnTeamLaunched: boolean;
notifyOnToolApproval: boolean;
+ autoResumeOnRateLimit: boolean;
statusChangeOnlySolo: boolean;
statusChangeStatuses: string[];
triggers: AppConfig['notifications']['triggers'];
@@ -195,6 +196,7 @@ export function useSettingsConfig(): UseSettingsConfigReturn {
notifyOnCrossTeamMessage: displayConfig?.notifications?.notifyOnCrossTeamMessage ?? true,
notifyOnTeamLaunched: displayConfig?.notifications?.notifyOnTeamLaunched ?? true,
notifyOnToolApproval: displayConfig?.notifications?.notifyOnToolApproval ?? true,
+ autoResumeOnRateLimit: displayConfig?.notifications?.autoResumeOnRateLimit ?? false,
statusChangeOnlySolo: displayConfig?.notifications?.statusChangeOnlySolo ?? true,
statusChangeStatuses: displayConfig?.notifications?.statusChangeStatuses ?? [
'in_progress',
diff --git a/src/renderer/components/settings/hooks/useSettingsHandlers.ts b/src/renderer/components/settings/hooks/useSettingsHandlers.ts
index 94245140..ac027199 100644
--- a/src/renderer/components/settings/hooks/useSettingsHandlers.ts
+++ b/src/renderer/components/settings/hooks/useSettingsHandlers.ts
@@ -311,6 +311,7 @@ export function useSettingsHandlers({
notifyOnCrossTeamMessage: true,
notifyOnTeamLaunched: true,
notifyOnToolApproval: true,
+ autoResumeOnRateLimit: false,
statusChangeOnlySolo: true,
statusChangeStatuses: ['in_progress', 'completed'],
triggers: defaultTriggers,
diff --git a/src/renderer/components/settings/sections/AdvancedSection.tsx b/src/renderer/components/settings/sections/AdvancedSection.tsx
index 086599af..949a3c00 100644
--- a/src/renderer/components/settings/sections/AdvancedSection.tsx
+++ b/src/renderer/components/settings/sections/AdvancedSection.tsx
@@ -172,7 +172,7 @@ export const AdvancedSection = ({
- Claude Agent Teams UI
+ Agent Teams UI
{isElectron && (
{
!cliStatus && cliStatusLoading && multimodelEnabled
? createLoadingMultimodelCliStatus()
: cliStatus;
+ const canOpenExtensions = effectiveCliStatus?.installed === true;
const showInstalledControls =
effectiveCliStatus !== null && (installerState === 'idle' || installerState === 'completed');
@@ -396,7 +397,7 @@ export const CliStatusSection = (): React.JSX.Element | null => {
) : null}
{/* Extensions button — right-aligned */}
- {effectiveCliStatus.authLoggedIn && (
+ {canOpenExtensions && (
void;
@@ -360,6 +361,17 @@ export const NotificationsSection = ({
disabled={saving || !safeConfig.notifications.enabled}
/>
+ }
+ >
+ onNotificationToggle('autoResumeOnRateLimit', v)}
+ disabled={saving || !safeConfig.notifications.enabled}
+ />
+
{/* Task Status Change Notifications — nested within team card */}
diff --git a/src/renderer/components/sidebar/GlobalTaskList.tsx b/src/renderer/components/sidebar/GlobalTaskList.tsx
index 4903c720..188bc53f 100644
--- a/src/renderer/components/sidebar/GlobalTaskList.tsx
+++ b/src/renderer/components/sidebar/GlobalTaskList.tsx
@@ -21,6 +21,7 @@ import {
Check,
ChevronDown,
ChevronRight,
+ Folder,
ListTodo,
Pin,
Search,
@@ -56,7 +57,7 @@ function loadGroupingMode(): TaskGroupingMode {
} catch {
/* ignore */
}
- return 'none';
+ return 'project';
}
function saveGroupingMode(mode: TaskGroupingMode): void {
@@ -625,6 +626,7 @@ export const GlobalTaskList = ({
projectGroups.map((group) => {
if (group.tasks.length === 0) return null;
const isGroupCollapsed = projectCollapsed.isCollapsed(group.projectKey);
+ const groupColor = projectColor(group.projectLabel);
let lastTeam: string | null = null;
return (
@@ -639,14 +641,12 @@ export const GlobalTaskList = ({
) : (
)}
-
-
+
{group.projectLabel}
diff --git a/src/renderer/components/team/members/MemberCard.tsx b/src/renderer/components/team/members/MemberCard.tsx
index bcf7ebc2..3937e09f 100644
--- a/src/renderer/components/team/members/MemberCard.tsx
+++ b/src/renderer/components/team/members/MemberCard.tsx
@@ -111,8 +111,7 @@ export const MemberCard = ({
!isRemoved &&
presenceLabel === 'starting' &&
spawnLaunchState !== 'failed_to_start' &&
- !activityTask &&
- !runtimeSummary;
+ !activityTask;
const showStartingBadge = !isRemoved && presenceLabel === 'starting' && !activityTask;
const showRuntimeAdvisoryBadge =
!isRemoved &&
diff --git a/src/renderer/components/team/messages/MessageComposer.tsx b/src/renderer/components/team/messages/MessageComposer.tsx
index eff3ceea..c461aa94 100644
--- a/src/renderer/components/team/messages/MessageComposer.tsx
+++ b/src/renderer/components/team/messages/MessageComposer.tsx
@@ -237,7 +237,8 @@ export const MessageComposer = ({
buildSlashCommandSuggestions(
getSuggestedSlashCommandsForProvider(leadProviderId),
projectSkills,
- userSkills
+ userSkills,
+ leadProviderId
),
[leadProviderId, projectSkills, userSkills]
);
diff --git a/src/renderer/index.html b/src/renderer/index.html
index ced0a449..1f849482 100644
--- a/src/renderer/index.html
+++ b/src/renderer/index.html
@@ -4,7 +4,7 @@
- Claude Agent Teams UI
+ Agent Teams UI