diff --git a/src/renderer/components/extensions/ExtensionStoreView.tsx b/src/renderer/components/extensions/ExtensionStoreView.tsx
index 8f0c7130..fbcf7d5e 100644
--- a/src/renderer/components/extensions/ExtensionStoreView.tsx
+++ b/src/renderer/components/extensions/ExtensionStoreView.tsx
@@ -30,6 +30,7 @@ import { ExtensionsSubTabTrigger } from './ExtensionsSubTabTrigger';
export const ExtensionStoreView = (): React.JSX.Element => {
const tabId = useTabIdOptional();
const fetchPluginCatalog = useStore((s) => s.fetchPluginCatalog);
+ const fetchCliStatus = useStore((s) => s.fetchCliStatus);
const fetchApiKeys = useStore((s) => s.fetchApiKeys);
const fetchSkillsCatalog = useStore((s) => s.fetchSkillsCatalog);
const mcpBrowse = useStore((s) => s.mcpBrowse);
@@ -38,7 +39,9 @@ export const ExtensionStoreView = (): React.JSX.Element => {
const mcpBrowseLoading = useStore((s) => s.mcpBrowseLoading);
const skillsLoading = useStore((s) => s.skillsLoading);
const cliStatus = useStore((s) => s.cliStatus);
+ const cliStatusLoading = useStore((s) => s.cliStatusLoading);
const cliInstalled = cliStatus?.installed ?? true; // assume installed until checked
+ const openDashboard = useStore((s) => s.openDashboard);
const hasOngoingSessions = useStore((s) => s.sessions.some((sess) => sess.isOngoing));
const projects = useStore((s) => s.projects);
const extensionsTabProjectId = useStore((s) =>
@@ -97,6 +100,10 @@ export const ExtensionStoreView = (): React.JSX.Element => {
void fetchPluginCatalog(projectPath ?? undefined);
}, [fetchPluginCatalog, projectPath]);
+ useEffect(() => {
+ void fetchCliStatus();
+ }, [fetchCliStatus]);
+
// Fetch MCP installed state on mount
useEffect(() => {
void mcpFetchInstalled(projectPath ?? undefined);
@@ -121,6 +128,71 @@ export const ExtensionStoreView = (): React.JSX.Element => {
}, [fetchPluginCatalog, fetchSkillsCatalog, mcpBrowse, mcpFetchInstalled, projectPath]);
const isRefreshing = pluginCatalogLoading || mcpBrowseLoading || skillsLoading;
+ const cliStatusBanner = useMemo(() => {
+ if (cliStatusLoading || cliStatus === null) {
+ return (
+
+
+
+
Checking Claude CLI availability
+
+ Extensions need Claude CLI to install plugins, run MCP servers, and validate auth.
+
+
+
+ );
+ }
+
+ if (!cliStatus.installed) {
+ return (
+
+
+
+
Claude CLI is not available
+
+ Plugin installs are disabled until Claude CLI is installed. Open the Dashboard to
+ install it and retry.
+
+
+
+ Open Dashboard
+
+
+ );
+ }
+
+ if (!cliStatus.authLoggedIn) {
+ return (
+
+
+
+
Claude CLI needs sign-in
+
+ Claude CLI was found
+ {cliStatus.installedVersion ? ` (${cliStatus.installedVersion})` : ''}, but plugin
+ installs are disabled until you sign in from the Dashboard.
+
+
+
+ Open Dashboard
+
+
+ );
+ }
+
+ return (
+
+
+
+
Claude CLI is ready
+
+ Plugins can be installed from this page
+ {cliStatus.installedVersion ? ` using Claude CLI ${cliStatus.installedVersion}` : ''}.
+
+
+
+ );
+ }, [cliStatus, cliStatusLoading, openDashboard]);
// Browser mode guard
if (!api.plugins && !api.mcpRegistry && !api.skills) {
@@ -138,6 +210,7 @@ export const ExtensionStoreView = (): React.JSX.Element => {
return (
+ {cliStatusBanner}
{/* Header */}
diff --git a/src/renderer/components/extensions/common/InstallButton.tsx b/src/renderer/components/extensions/common/InstallButton.tsx
index 5d45a257..ea609745 100644
--- a/src/renderer/components/extensions/common/InstallButton.tsx
+++ b/src/renderer/components/extensions/common/InstallButton.tsx
@@ -37,8 +37,20 @@ export const InstallButton = ({
errorMessage,
}: InstallButtonProps) => {
const cliStatus = useStore((s) => s.cliStatus);
- const cliMissing = cliStatus !== null && !cliStatus.installed;
- const isDisabled = disabled || cliMissing;
+ const cliStatusLoading = useStore((s) => s.cliStatusLoading);
+ const cliUnknown = cliStatus === null;
+ const cliMissing = cliStatus?.installed === false;
+ const authMissing = cliStatus?.installed === true && !cliStatus.authLoggedIn;
+ const disableReason = cliStatusLoading
+ ? 'Checking Claude CLI status...'
+ : cliUnknown
+ ? 'Checking Claude CLI availability...'
+ : cliMissing
+ ? 'Claude CLI required. Install it from the Dashboard.'
+ : authMissing
+ ? 'Claude CLI is installed but not signed in. Open the Dashboard to sign in.'
+ : null;
+ const isDisabled = disabled || Boolean(disableReason);
const [lastAction, setLastAction] = useState<'install' | 'uninstall' | null>(null);
useEffect(() => {
@@ -91,23 +103,30 @@ export const InstallButton = ({
);
- if (errorMessage) {
+ const tooltipMessage = disableReason ?? errorMessage;
+
+ if (tooltipMessage) {
return (
-
-
-
- {retryButton}
-
- {errorMessage}
-
-
+
+
+
+
+ {retryButton}
+
+ {tooltipMessage}
+
+
+ {errorMessage && !disableReason ? (
+
{errorMessage}
+ ) : null}
+
);
}
return retryButton;
}
- // idle — wrap in tooltip when CLI missing
+ // idle — wrap in tooltip when install is unavailable
const button = isInstalled ? (
);
- if (cliMissing) {
+ if (disableReason) {
return (
{button}
- Claude CLI required
+ {disableReason}
);
diff --git a/src/renderer/components/extensions/plugins/PluginCard.tsx b/src/renderer/components/extensions/plugins/PluginCard.tsx
index be2a7379..3a2a9bfb 100644
--- a/src/renderer/components/extensions/plugins/PluginCard.tsx
+++ b/src/renderer/components/extensions/plugins/PluginCard.tsx
@@ -98,7 +98,7 @@ export const PluginCard = ({ plugin, index, onClick }: PluginCardProps): React.J
{/* Footer: author + version + install button */}
-
+
{plugin.author?.name ?? 'Unknown author'}
{plugin.version && (
diff --git a/src/renderer/store/slices/extensionsSlice.ts b/src/renderer/store/slices/extensionsSlice.ts
index 067a1eba..7d283fb1 100644
--- a/src/renderer/store/slices/extensionsSlice.ts
+++ b/src/renderer/store/slices/extensionsSlice.ts
@@ -4,6 +4,7 @@
*/
import { api } from '@renderer/api';
+import { CLI_NOT_FOUND_MESSAGE } from '@shared/constants/cli';
import type { AppState } from '../types';
import type {
@@ -143,6 +144,10 @@ function getSkillsCatalogKey(projectPath?: string): string {
/** Duration to show "success" state before returning to idle */
const SUCCESS_DISPLAY_MS = 2_000;
+const CLI_AUTH_REQUIRED_MESSAGE =
+ 'Claude CLI is installed but not signed in. Go to the Dashboard and sign in to enable plugin installs.';
+const CLI_STATUS_UNKNOWN_MESSAGE =
+ 'Unable to verify Claude CLI status. Open the Dashboard and check the CLI before retrying.';
export const createExtensionsSlice: StateCreator
= (
set,
@@ -552,8 +557,36 @@ export const createExtensionsSlice: StateCreator {
if (!api.plugins) return;
+ const preflightState = get();
+ if (preflightState.cliStatus === null || preflightState.cliStatusLoading) {
+ try {
+ await preflightState.fetchCliStatus();
+ } catch {
+ // fetchCliStatus stores the error in cliStatusError; map to a user-facing install error below.
+ }
+ }
+
+ const cliStatus = get().cliStatus;
+ const preflightError =
+ cliStatus === null
+ ? CLI_STATUS_UNKNOWN_MESSAGE
+ : !cliStatus.installed
+ ? CLI_NOT_FOUND_MESSAGE
+ : !cliStatus.authLoggedIn
+ ? CLI_AUTH_REQUIRED_MESSAGE
+ : null;
+
+ if (preflightError) {
+ set((prev) => ({
+ pluginInstallProgress: { ...prev.pluginInstallProgress, [request.pluginId]: 'error' },
+ installErrors: { ...prev.installErrors, [request.pluginId]: preflightError },
+ }));
+ return;
+ }
+
set((prev) => ({
pluginInstallProgress: { ...prev.pluginInstallProgress, [request.pluginId]: 'pending' },
+ installErrors: { ...prev.installErrors, [request.pluginId]: '' },
}));
try {