mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-18 19:50: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
151
ui/src/adapters/adapter-display-registry.ts
Normal file
151
ui/src/adapters/adapter-display-registry.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
/**
|
||||
* Single source of truth for adapter display metadata.
|
||||
*
|
||||
* Built-in adapters have entries in `adapterDisplayMap`. External (plugin)
|
||||
* adapters get sensible defaults derived from their type string via
|
||||
* `getAdapterDisplay()`.
|
||||
*/
|
||||
import type { ComponentType } from "react";
|
||||
import {
|
||||
Bot,
|
||||
Code,
|
||||
Gem,
|
||||
MousePointer2,
|
||||
Sparkles,
|
||||
Terminal,
|
||||
Cpu,
|
||||
} from "lucide-react";
|
||||
import { OpenCodeLogoIcon } from "@/components/OpenCodeLogoIcon";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Type suffix parsing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const TYPE_SUFFIXES: Record<string, string> = {
|
||||
_local: "local",
|
||||
_gateway: "gateway",
|
||||
};
|
||||
|
||||
function getTypeSuffix(type: string): string | null {
|
||||
for (const [suffix, mode] of Object.entries(TYPE_SUFFIXES)) {
|
||||
if (type.endsWith(suffix)) return mode;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function withSuffix(label: string, suffix: string | null): string {
|
||||
return suffix ? `${label} (${suffix})` : label;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Display metadata per adapter type
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface AdapterDisplayInfo {
|
||||
label: string;
|
||||
description: string;
|
||||
icon: ComponentType<{ className?: string }>;
|
||||
recommended?: boolean;
|
||||
comingSoon?: boolean;
|
||||
disabledLabel?: string;
|
||||
}
|
||||
|
||||
const adapterDisplayMap: Record<string, AdapterDisplayInfo> = {
|
||||
claude_local: {
|
||||
label: "Claude Code",
|
||||
description: "Local Claude agent",
|
||||
icon: Sparkles,
|
||||
recommended: true,
|
||||
},
|
||||
codex_local: {
|
||||
label: "Codex",
|
||||
description: "Local Codex agent",
|
||||
icon: Code,
|
||||
recommended: true,
|
||||
},
|
||||
gemini_local: {
|
||||
label: "Gemini CLI",
|
||||
description: "Local Gemini agent",
|
||||
icon: Gem,
|
||||
},
|
||||
opencode_local: {
|
||||
label: "OpenCode",
|
||||
description: "Local multi-provider agent",
|
||||
icon: OpenCodeLogoIcon,
|
||||
},
|
||||
pi_local: {
|
||||
label: "Pi",
|
||||
description: "Local Pi agent",
|
||||
icon: Terminal,
|
||||
},
|
||||
cursor: {
|
||||
label: "Cursor",
|
||||
description: "Local Cursor agent",
|
||||
icon: MousePointer2,
|
||||
},
|
||||
openclaw_gateway: {
|
||||
label: "OpenClaw Gateway",
|
||||
description: "Invoke OpenClaw via gateway protocol",
|
||||
icon: Bot,
|
||||
comingSoon: true,
|
||||
disabledLabel: "Configure OpenClaw within the App",
|
||||
},
|
||||
process: {
|
||||
label: "Process",
|
||||
description: "Internal process adapter",
|
||||
icon: Cpu,
|
||||
comingSoon: true,
|
||||
},
|
||||
http: {
|
||||
label: "HTTP",
|
||||
description: "Internal HTTP adapter",
|
||||
icon: Cpu,
|
||||
comingSoon: true,
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function humanizeType(type: string): string {
|
||||
// Strip known type suffixes so "droid_local" → "Droid", not "Droid Local"
|
||||
let base = type;
|
||||
for (const suffix of Object.keys(TYPE_SUFFIXES)) {
|
||||
if (base.endsWith(suffix)) {
|
||||
base = base.slice(0, -suffix.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return base.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
export function getAdapterLabel(type: string): string {
|
||||
const base = adapterDisplayMap[type]?.label ?? humanizeType(type);
|
||||
return withSuffix(base, getTypeSuffix(type));
|
||||
}
|
||||
|
||||
export function getAdapterLabels(): Record<string, string> {
|
||||
const suffixed: Record<string, string> = {};
|
||||
for (const [type, info] of Object.entries(adapterDisplayMap)) {
|
||||
suffixed[type] = withSuffix(info.label, getTypeSuffix(type));
|
||||
}
|
||||
return suffixed;
|
||||
}
|
||||
|
||||
export function getAdapterDisplay(type: string): AdapterDisplayInfo {
|
||||
const known = adapterDisplayMap[type];
|
||||
if (known) return known;
|
||||
|
||||
const suffix = getTypeSuffix(type);
|
||||
const label = withSuffix(humanizeType(type), suffix);
|
||||
return {
|
||||
label,
|
||||
description: suffix ? `External ${suffix} adapter` : "External adapter",
|
||||
icon: Cpu,
|
||||
};
|
||||
}
|
||||
|
||||
export function isKnownAdapterType(type: string): boolean {
|
||||
return type in adapterDisplayMap;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue