mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-15 23:31:29 +00:00
feat(claude-code): two-step add-marketplace flow with skill confirmation
Add Marketplace used to submit and close the modal immediately, giving no visibility into what actually got imported (worse now that sync can take a few seconds for larger repos). It's now two steps: fill in the repo, click Next, and the modal stays open showing an importing state while the sync runs, then a scrollable list of every skill that was actually loaded (or the error, with a way to go back and retry) before you dismiss it. Verified live: registering a 53-skill repo through the new flow shows the full skill list with visibility badges before Done is clicked.
This commit is contained in:
parent
fcd285facd
commit
62547d67f0
2 changed files with 142 additions and 41 deletions
|
|
@ -1,7 +1,9 @@
|
|||
import React, { useState } from "react";
|
||||
import { Modal, Form, Input, Button } from "antd";
|
||||
import { registerClaudeCodeMarketplace } from "@/components/networking";
|
||||
import { Modal, Form, Input, Button, Spin, List, Tag } from "antd";
|
||||
import { CheckCircleFilled, CloseCircleFilled, ExclamationCircleFilled } from "@ant-design/icons";
|
||||
import { registerClaudeCodeMarketplace, getClaudeCodePluginsList } from "@/components/networking";
|
||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
import { MarketplaceSource, PluginListItem } from "@/components/claude_code_plugins/types";
|
||||
|
||||
interface AddMarketplaceFormProps {
|
||||
visible: boolean;
|
||||
|
|
@ -15,13 +17,30 @@ interface AddMarketplaceFormValues {
|
|||
name?: string;
|
||||
}
|
||||
|
||||
type Step = "form" | "importing" | "result";
|
||||
|
||||
const AddMarketplaceForm: React.FC<AddMarketplaceFormProps> = ({ visible, onClose, accessToken, onSuccess }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [step, setStep] = useState<Step>("form");
|
||||
const [marketplace, setMarketplace] = useState<MarketplaceSource | null>(null);
|
||||
const [loadedSkills, setLoadedSkills] = useState<PluginListItem[]>([]);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const resetAndClose = () => {
|
||||
form.resetFields();
|
||||
setStep("form");
|
||||
setMarketplace(null);
|
||||
setLoadedSkills([]);
|
||||
setErrorMessage(null);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
onClose();
|
||||
// Once import has started, the marketplace already exists server-side
|
||||
// regardless of whether this dialog stays open - closing early shouldn't
|
||||
// look like nothing happened, so still refresh the lists behind it.
|
||||
if (step !== "form") onSuccess();
|
||||
resetAndClose();
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: AddMarketplaceFormValues) => {
|
||||
|
|
@ -30,56 +49,137 @@ const AddMarketplaceForm: React.FC<AddMarketplaceFormProps> = ({ visible, onClos
|
|||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
setStep("importing");
|
||||
setErrorMessage(null);
|
||||
try {
|
||||
await registerClaudeCodeMarketplace(accessToken, {
|
||||
const response = await registerClaudeCodeMarketplace(accessToken, {
|
||||
source: values.source.trim(),
|
||||
...(values.name?.trim() ? { name: values.name.trim() } : {}),
|
||||
});
|
||||
NotificationsManager.success("Marketplace imported successfully");
|
||||
form.resetFields();
|
||||
setMarketplace(response.marketplace);
|
||||
|
||||
const skillsResponse = await getClaudeCodePluginsList(accessToken, false);
|
||||
const prefix = `${response.marketplace.name}--`;
|
||||
setLoadedSkills(skillsResponse.plugins.filter((p: PluginListItem) => p.name.startsWith(prefix)));
|
||||
|
||||
onSuccess();
|
||||
onClose();
|
||||
setStep("result");
|
||||
} catch (error) {
|
||||
console.error("Error registering marketplace:", error);
|
||||
const reason = error instanceof Error && error.message ? error.message : "Failed to import marketplace";
|
||||
NotificationsManager.error(`Failed to import marketplace: ${reason}`);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
setErrorMessage(error instanceof Error && error.message ? error.message : "Failed to import marketplace");
|
||||
setStep("result");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal title="Add Marketplace" open={visible} onCancel={handleCancel} footer={null} width={520} className="top-8">
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit} className="mt-4">
|
||||
<Form.Item
|
||||
label="Repository"
|
||||
name="source"
|
||||
rules={[{ required: true, message: "Please enter a repository (org/repo) or URL" }]}
|
||||
tooltip="A GitHub org/repo (e.g. anthropics/claude-code-marketplace) or a full git URL"
|
||||
>
|
||||
<Input placeholder="org/repo or https://github.com/org/repo" className="rounded-lg" />
|
||||
</Form.Item>
|
||||
const renderForm = () => (
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit} className="mt-4">
|
||||
<Form.Item
|
||||
label="Repository"
|
||||
name="source"
|
||||
rules={[{ required: true, message: "Please enter a repository (org/repo) or URL" }]}
|
||||
tooltip="A GitHub org/repo (e.g. anthropics/claude-code-marketplace) or a full git URL"
|
||||
>
|
||||
<Input placeholder="org/repo or https://github.com/org/repo" className="rounded-lg" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Name (Optional)"
|
||||
name="name"
|
||||
tooltip="Marketplace identifier used to namespace its skills. Defaults to the repository name"
|
||||
>
|
||||
<Input placeholder="my-marketplace" className="rounded-lg" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Name (Optional)"
|
||||
name="name"
|
||||
tooltip="Marketplace identifier used to namespace its skills. Defaults to the repository name"
|
||||
>
|
||||
<Input placeholder="my-marketplace" className="rounded-lg" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item className="mb-0 mt-6">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button onClick={handleCancel} disabled={isSubmitting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Importing..." : "Add Marketplace"}
|
||||
<Form.Item className="mb-0 mt-6">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button onClick={handleCancel}>Cancel</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
|
||||
const renderImporting = () => (
|
||||
<div className="flex flex-col items-center justify-center gap-4 py-12">
|
||||
<Spin size="large" />
|
||||
<p className="text-gray-600">Importing marketplace and loading its skills…</p>
|
||||
<p className="text-xs text-gray-400">This can take a few seconds for repositories with many skills.</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderResult = () => {
|
||||
if (errorMessage) {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="flex items-start gap-2 rounded-lg bg-red-50 p-4">
|
||||
<CloseCircleFilled className="mt-0.5 text-red-500" />
|
||||
<div>
|
||||
<p className="font-medium text-red-700">Import failed</p>
|
||||
<p className="text-sm text-red-600">{errorMessage}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end gap-2">
|
||||
<Button onClick={resetAndClose}>Close</Button>
|
||||
<Button type="primary" onClick={() => setStep("form")}>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const skippedCount = marketplace?.skipped_count ?? 0;
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<div className="flex items-start gap-2 rounded-lg bg-green-50 p-4">
|
||||
<CheckCircleFilled className="mt-0.5 text-green-500" />
|
||||
<div>
|
||||
<p className="font-medium text-green-700">
|
||||
{loadedSkills.length} skill{loadedSkills.length === 1 ? "" : "s"} loaded from "{marketplace?.name}
|
||||
"
|
||||
</p>
|
||||
{skippedCount > 0 && (
|
||||
<p className="mt-1 flex items-center gap-1 text-sm text-amber-600">
|
||||
<ExclamationCircleFilled />
|
||||
{skippedCount} skill{skippedCount === 1 ? "" : "s"} could not be fetched after retries. Use
|
||||
"Sync now" on the marketplace to try again.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 max-h-80 overflow-y-auto rounded-lg border border-gray-200">
|
||||
<List
|
||||
size="small"
|
||||
dataSource={loadedSkills}
|
||||
renderItem={(skill) => (
|
||||
<List.Item className="px-4">
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<span className="font-mono text-sm">{skill.name}</span>
|
||||
<Tag color={skill.enabled ? "green" : "default"}>{skill.enabled ? "public" : "private"}</Tag>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end">
|
||||
<Button type="primary" onClick={resetAndClose}>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal title="Add Marketplace" open={visible} onCancel={handleCancel} footer={null} width={560} className="top-8">
|
||||
{step === "form" && renderForm()}
|
||||
{step === "importing" && renderImporting()}
|
||||
{step === "result" && renderResult()}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ export interface MarketplaceSource {
|
|||
sync_error?: string;
|
||||
last_synced_at?: string;
|
||||
plugin_count: number;
|
||||
skipped_count: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue