Improve Extensions CLI preflight and plugin install diagnostics

Refs: https://github.com/777genius/claude_agent_teams_ui/issues/37
This commit is contained in:
iliya 2026-04-01 16:49:07 +03:00
parent e26310870c
commit 21513bb6f8
4 changed files with 140 additions and 15 deletions

View file

@ -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 (
<div className="bg-surface/70 mx-4 mt-3 flex items-start gap-3 rounded-md border border-border px-4 py-3">
<Info className="mt-0.5 size-4 shrink-0 text-text-secondary" />
<div>
<p className="text-sm font-medium text-text">Checking Claude CLI availability</p>
<p className="mt-0.5 text-xs text-text-muted">
Extensions need Claude CLI to install plugins, run MCP servers, and validate auth.
</p>
</div>
</div>
);
}
if (!cliStatus.installed) {
return (
<div className="mx-4 mt-3 flex items-start gap-3 rounded-md border border-amber-500/30 bg-amber-500/5 px-4 py-3">
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-amber-400" />
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-amber-300">Claude CLI is not available</p>
<p className="mt-0.5 text-xs text-text-muted">
Plugin installs are disabled until Claude CLI is installed. Open the Dashboard to
install it and retry.
</p>
</div>
<Button size="sm" variant="outline" onClick={openDashboard}>
Open Dashboard
</Button>
</div>
);
}
if (!cliStatus.authLoggedIn) {
return (
<div className="mx-4 mt-3 flex items-start gap-3 rounded-md border border-amber-500/30 bg-amber-500/5 px-4 py-3">
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-amber-400" />
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-amber-300">Claude CLI needs sign-in</p>
<p className="mt-0.5 text-xs text-text-muted">
Claude CLI was found
{cliStatus.installedVersion ? ` (${cliStatus.installedVersion})` : ''}, but plugin
installs are disabled until you sign in from the Dashboard.
</p>
</div>
<Button size="sm" variant="outline" onClick={openDashboard}>
Open Dashboard
</Button>
</div>
);
}
return (
<div className="mx-4 mt-3 flex items-start gap-3 rounded-md border border-emerald-500/30 bg-emerald-500/5 px-4 py-3">
<Info className="mt-0.5 size-4 shrink-0 text-emerald-300" />
<div>
<p className="text-sm font-medium text-emerald-300">Claude CLI is ready</p>
<p className="mt-0.5 text-xs text-text-muted">
Plugins can be installed from this page
{cliStatus.installedVersion ? ` using Claude CLI ${cliStatus.installedVersion}` : ''}.
</p>
</div>
</div>
);
}, [cliStatus, cliStatusLoading, openDashboard]);
// Browser mode guard
if (!api.plugins && !api.mcpRegistry && !api.skills) {
@ -138,6 +210,7 @@ export const ExtensionStoreView = (): React.JSX.Element => {
return (
<TooltipProvider>
<div className="flex flex-1 flex-col overflow-hidden">
{cliStatusBanner}
<div className="flex-1 overflow-y-auto">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4">

View file

@ -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 = ({
</Button>
);
if (errorMessage) {
const tooltipMessage = disableReason ?? errorMessage;
if (tooltipMessage) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span tabIndex={0}>{retryButton}</span>
</TooltipTrigger>
<TooltipContent className="max-w-64 text-red-300">{errorMessage}</TooltipContent>
</Tooltip>
</TooltipProvider>
<div className="flex max-w-64 flex-col items-end gap-1">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span tabIndex={0}>{retryButton}</span>
</TooltipTrigger>
<TooltipContent className="max-w-64 text-red-300">{tooltipMessage}</TooltipContent>
</Tooltip>
</TooltipProvider>
{errorMessage && !disableReason ? (
<p className="text-right text-[11px] leading-4 text-red-300">{errorMessage}</p>
) : null}
</div>
);
}
return retryButton;
}
// idle — wrap in tooltip when CLI missing
// idle — wrap in tooltip when install is unavailable
const button = isInstalled ? (
<Button
size={size}
@ -138,14 +157,14 @@ export const InstallButton = ({
</Button>
);
if (cliMissing) {
if (disableReason) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span tabIndex={0}>{button}</span>
</TooltipTrigger>
<TooltipContent>Claude CLI required</TooltipContent>
<TooltipContent className="max-w-64">{disableReason}</TooltipContent>
</Tooltip>
</TooltipProvider>
);

View file

@ -98,7 +98,7 @@ export const PluginCard = ({ plugin, index, onClick }: PluginCardProps): React.J
</p>
{/* Footer: author + version + install button */}
<div className="flex items-center justify-between gap-2">
<div className="flex items-start justify-between gap-2">
<div className="flex min-w-0 items-center gap-3 text-xs text-text-muted">
<span className="truncate">{plugin.author?.name ?? 'Unknown author'}</span>
{plugin.version && (

View file

@ -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<AppState, [], [], ExtensionsSlice> = (
set,
@ -552,8 +557,36 @@ export const createExtensionsSlice: StateCreator<AppState, [], [], ExtensionsSli
installPlugin: async (request: PluginInstallRequest) => {
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 {