mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-17 03:10:38 +09:00
feat(adapters): external adapter plugin system with dynamic UI parser
- Plugin loader: install/reload/remove/reinstall external adapters from npm packages or local directories - Plugin store persisted at ~/.paperclip/adapter-plugins.json - Self-healing UI parser resolution with version caching - UI: Adapter Manager page, dynamic loader, display registry with humanized names for unknown adapter types - Dev watch: exclude adapter-plugins dir from tsx watcher to prevent mid-request server restarts during reinstall - All consumer fallbacks use getAdapterLabel() for consistent display - AdapterTypeDropdown uses controlled open state for proper close behavior - Remove hermes-local from built-in UI (externalized to plugin) - Add docs for external adapters and UI parser contract
This commit is contained in:
parent
f8452a4520
commit
14d59da316
72 changed files with 4102 additions and 585 deletions
|
|
@ -23,6 +23,9 @@ import {
|
|||
extractProviderIdWithFallback
|
||||
} from "../lib/model-utils";
|
||||
import { getUIAdapter } from "../adapters";
|
||||
import { listUIAdapters } from "../adapters";
|
||||
import { useDisabledAdaptersSync } from "../adapters/use-disabled-adapters";
|
||||
import { getAdapterDisplay } from "../adapters/adapter-display-registry";
|
||||
import { defaultCreateValues } from "./agent-config-defaults";
|
||||
import { parseOnboardingGoalInput } from "../lib/onboarding-goal";
|
||||
import {
|
||||
|
|
@ -38,37 +41,22 @@ import { DEFAULT_CURSOR_LOCAL_MODEL } from "@paperclipai/adapter-cursor-local";
|
|||
import { DEFAULT_GEMINI_LOCAL_MODEL } from "@paperclipai/adapter-gemini-local";
|
||||
import { resolveRouteOnboardingOptions } from "../lib/onboarding-route";
|
||||
import { AsciiArtAnimation } from "./AsciiArtAnimation";
|
||||
import { OpenCodeLogoIcon } from "./OpenCodeLogoIcon";
|
||||
import {
|
||||
Building2,
|
||||
Bot,
|
||||
Code,
|
||||
Gem,
|
||||
ListTodo,
|
||||
Rocket,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Terminal,
|
||||
Sparkles,
|
||||
MousePointer2,
|
||||
Check,
|
||||
Loader2,
|
||||
ChevronDown,
|
||||
X
|
||||
} from "lucide-react";
|
||||
import { HermesIcon } from "./HermesIcon";
|
||||
|
||||
|
||||
type Step = 1 | 2 | 3 | 4;
|
||||
type AdapterType =
|
||||
| "claude_local"
|
||||
| "codex_local"
|
||||
| "gemini_local"
|
||||
| "hermes_local"
|
||||
| "opencode_local"
|
||||
| "pi_local"
|
||||
| "cursor"
|
||||
| "http"
|
||||
| "openclaw_gateway";
|
||||
type AdapterType = string;
|
||||
|
||||
const DEFAULT_TASK_DESCRIPTION = `You are the CEO. You set the direction for the company.
|
||||
|
||||
|
|
@ -85,6 +73,9 @@ export function OnboardingWizard() {
|
|||
const { companyPrefix } = useParams<{ companyPrefix?: string }>();
|
||||
const [routeDismissed, setRouteDismissed] = useState(false);
|
||||
|
||||
// Sync disabled adapter types from server so adapter grid filters them out
|
||||
const disabledTypes = useDisabledAdaptersSync();
|
||||
|
||||
const routeOnboardingOptions =
|
||||
companyPrefix && companiesLoading
|
||||
? null
|
||||
|
|
@ -206,29 +197,33 @@ export function OnboardingWizard() {
|
|||
queryFn: () => agentsApi.adapterModels(createdCompanyId!, adapterType),
|
||||
enabled: Boolean(createdCompanyId) && effectiveOnboardingOpen && step === 2
|
||||
});
|
||||
const isLocalAdapter =
|
||||
adapterType === "claude_local" ||
|
||||
adapterType === "codex_local" ||
|
||||
adapterType === "gemini_local" ||
|
||||
adapterType === "hermes_local" ||
|
||||
adapterType === "opencode_local" ||
|
||||
adapterType === "pi_local" ||
|
||||
adapterType === "cursor";
|
||||
const NONLOCAL_TYPES = new Set(["process", "http", "openclaw_gateway"]);
|
||||
const isLocalAdapter = !NONLOCAL_TYPES.has(adapterType);
|
||||
|
||||
// Build adapter grids dynamically from the UI registry + display metadata.
|
||||
// External/plugin adapters automatically appear with generic defaults.
|
||||
const { recommendedAdapters, moreAdapters } = useMemo(() => {
|
||||
const SYSTEM_ADAPTER_TYPES = new Set(["process", "http"]);
|
||||
const all = listUIAdapters()
|
||||
.filter((a) => !SYSTEM_ADAPTER_TYPES.has(a.type) && !disabledTypes.has(a.type))
|
||||
.map((a) => ({ ...getAdapterDisplay(a.type), type: a.type }));
|
||||
|
||||
return {
|
||||
recommendedAdapters: all.filter((a) => a.recommended),
|
||||
moreAdapters: all.filter((a) => !a.recommended),
|
||||
};
|
||||
}, [disabledTypes]);
|
||||
const COMMAND_PLACEHOLDERS: Record<string, string> = {
|
||||
claude_local: "claude",
|
||||
codex_local: "codex",
|
||||
gemini_local: "gemini",
|
||||
pi_local: "pi",
|
||||
cursor: "agent",
|
||||
opencode_local: "opencode",
|
||||
};
|
||||
const effectiveAdapterCommand =
|
||||
command.trim() ||
|
||||
(adapterType === "codex_local"
|
||||
? "codex"
|
||||
: adapterType === "gemini_local"
|
||||
? "gemini"
|
||||
: adapterType === "hermes_local"
|
||||
? "hermes"
|
||||
: adapterType === "pi_local"
|
||||
? "pi"
|
||||
: adapterType === "cursor"
|
||||
? "agent"
|
||||
: adapterType === "opencode_local"
|
||||
? "opencode"
|
||||
: "claude");
|
||||
(COMMAND_PLACEHOLDERS[adapterType] ?? adapterType.replace(/_local$/, ""));
|
||||
|
||||
useEffect(() => {
|
||||
if (step !== 2) return;
|
||||
|
|
@ -759,32 +754,17 @@ export function OnboardingWizard() {
|
|||
Adapter type
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{[
|
||||
{
|
||||
value: "claude_local" as const,
|
||||
label: "Claude Code",
|
||||
icon: Sparkles,
|
||||
desc: "Local Claude agent",
|
||||
recommended: true
|
||||
},
|
||||
{
|
||||
value: "codex_local" as const,
|
||||
label: "Codex",
|
||||
icon: Code,
|
||||
desc: "Local Codex agent",
|
||||
recommended: true
|
||||
}
|
||||
].map((opt) => (
|
||||
{recommendedAdapters.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
key={opt.type}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-1.5 rounded-md border p-3 text-xs transition-colors relative",
|
||||
adapterType === opt.value
|
||||
adapterType === opt.type
|
||||
? "border-foreground bg-accent"
|
||||
: "border-border hover:bg-accent/50"
|
||||
)}
|
||||
onClick={() => {
|
||||
const nextType = opt.value as AdapterType;
|
||||
const nextType = opt.type;
|
||||
setAdapterType(nextType);
|
||||
if (nextType === "codex_local" && !model) {
|
||||
setModel(DEFAULT_CODEX_LOCAL_MODEL);
|
||||
|
|
@ -802,7 +782,7 @@ export function OnboardingWizard() {
|
|||
<opt.icon className="h-4 w-4" />
|
||||
<span className="font-medium">{opt.label}</span>
|
||||
<span className="text-muted-foreground text-[10px]">
|
||||
{opt.desc}
|
||||
{opt.description}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
|
|
@ -823,60 +803,21 @@ export function OnboardingWizard() {
|
|||
|
||||
{showMoreAdapters && (
|
||||
<div className="grid grid-cols-2 gap-2 mt-2">
|
||||
{[
|
||||
{
|
||||
value: "gemini_local" as const,
|
||||
label: "Gemini CLI",
|
||||
icon: Gem,
|
||||
desc: "Local Gemini agent"
|
||||
},
|
||||
{
|
||||
value: "opencode_local" as const,
|
||||
label: "OpenCode",
|
||||
icon: OpenCodeLogoIcon,
|
||||
desc: "Local multi-provider agent"
|
||||
},
|
||||
{
|
||||
value: "pi_local" as const,
|
||||
label: "Pi",
|
||||
icon: Terminal,
|
||||
desc: "Local Pi agent"
|
||||
},
|
||||
{
|
||||
value: "cursor" as const,
|
||||
label: "Cursor",
|
||||
icon: MousePointer2,
|
||||
desc: "Local Cursor agent"
|
||||
},
|
||||
{
|
||||
value: "hermes_local" as const,
|
||||
label: "Hermes Agent",
|
||||
icon: HermesIcon,
|
||||
desc: "Local multi-provider agent"
|
||||
},
|
||||
{
|
||||
value: "openclaw_gateway" as const,
|
||||
label: "OpenClaw Gateway",
|
||||
icon: Bot,
|
||||
desc: "Invoke OpenClaw via gateway protocol",
|
||||
comingSoon: true,
|
||||
disabledLabel: "Configure OpenClaw within the App"
|
||||
}
|
||||
].map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
disabled={!!opt.comingSoon}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-1.5 rounded-md border p-3 text-xs transition-colors relative",
|
||||
opt.comingSoon
|
||||
? "border-border opacity-40 cursor-not-allowed"
|
||||
: adapterType === opt.value
|
||||
? "border-foreground bg-accent"
|
||||
: "border-border hover:bg-accent/50"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (opt.comingSoon) return;
|
||||
const nextType = opt.value as AdapterType;
|
||||
{moreAdapters.map((opt) => (
|
||||
<button
|
||||
key={opt.type}
|
||||
disabled={!!opt.comingSoon}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-1.5 rounded-md border p-3 text-xs transition-colors relative",
|
||||
opt.comingSoon
|
||||
? "border-border opacity-40 cursor-not-allowed"
|
||||
: adapterType === opt.type
|
||||
? "border-foreground bg-accent"
|
||||
: "border-border hover:bg-accent/50"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (opt.comingSoon) return;
|
||||
const nextType = opt.type;
|
||||
setAdapterType(nextType);
|
||||
if (nextType === "gemini_local" && !model) {
|
||||
setModel(DEFAULT_GEMINI_LOCAL_MODEL);
|
||||
|
|
@ -899,9 +840,8 @@ export function OnboardingWizard() {
|
|||
<span className="font-medium">{opt.label}</span>
|
||||
<span className="text-muted-foreground text-[10px]">
|
||||
{opt.comingSoon
|
||||
? (opt as { disabledLabel?: string })
|
||||
.disabledLabel ?? "Coming soon"
|
||||
: opt.desc}
|
||||
? opt.disabledLabel ?? "Coming soon"
|
||||
: opt.description}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
|
|
@ -910,13 +850,7 @@ export function OnboardingWizard() {
|
|||
</div>
|
||||
|
||||
{/* Conditional adapter fields */}
|
||||
{(adapterType === "claude_local" ||
|
||||
adapterType === "codex_local" ||
|
||||
adapterType === "gemini_local" ||
|
||||
adapterType === "hermes_local" ||
|
||||
adapterType === "opencode_local" ||
|
||||
adapterType === "pi_local" ||
|
||||
adapterType === "cursor") && (
|
||||
{isLocalAdapter && (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue