mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-17 19:20:39 +09:00
feat(adapters): declarative config-schema API and UI for plugin adapters
Cherry-picked from feat/externalize-hermes-adapter. Resolved conflicts: kept Hermes as built-in on phase1 branch.
This commit is contained in:
parent
f884cbab78
commit
69a1593ff8
8 changed files with 392 additions and 10 deletions
41
AGENTS.md
41
AGENTS.md
|
|
@ -146,3 +146,44 @@ A change is done when all are true:
|
||||||
2. Typecheck, tests, and build pass
|
2. Typecheck, tests, and build pass
|
||||||
3. Contracts are synced across db/shared/server/ui
|
3. Contracts are synced across db/shared/server/ui
|
||||||
4. Docs updated when behavior or commands change
|
4. Docs updated when behavior or commands change
|
||||||
|
|
||||||
|
## 11. Fork-Specific: HenkDz/paperclip
|
||||||
|
|
||||||
|
This is a fork of `paperclipai/paperclip` with QoL patches and an **external-only** Hermes adapter story on branch `feat/externalize-hermes-adapter` ([tree](https://github.com/HenkDz/paperclip/tree/feat/externalize-hermes-adapter)).
|
||||||
|
|
||||||
|
### Branch Strategy
|
||||||
|
|
||||||
|
- `feat/externalize-hermes-adapter` → core has **no** `hermes-paperclip-adapter` dependency and **no** built-in `hermes_local` registration. Install Hermes via the Adapter Plugin manager (`@henkey/hermes-paperclip-adapter` or a `file:` path).
|
||||||
|
- Older fork branches may still document built-in Hermes; treat this file as authoritative for the externalize branch.
|
||||||
|
|
||||||
|
### Hermes (plugin only)
|
||||||
|
|
||||||
|
- Register through **Board → Adapter manager** (same as Droid). Type remains `hermes_local` once the package is loaded.
|
||||||
|
- UI uses generic **config-schema** + **ui-parser.js** from the package — no Hermes imports in `server/` or `ui/` source.
|
||||||
|
- Optional: `file:` entry in `~/.paperclip/adapter-plugins.json` for local dev of the adapter repo.
|
||||||
|
|
||||||
|
### Local Dev
|
||||||
|
|
||||||
|
- Fork runs on port 3101+ (auto-detects if 3100 is taken by upstream instance)
|
||||||
|
- `npx vite build` hangs on NTFS — use `node node_modules/vite/bin/vite.js build` instead
|
||||||
|
- Server startup from NTFS takes 30-60s — don't assume failure immediately
|
||||||
|
- Kill ALL paperclip processes before starting: `pkill -f "paperclip"; pkill -f "tsx.*index.ts"`
|
||||||
|
- Vite cache survives `rm -rf dist` — delete both: `rm -rf ui/dist ui/node_modules/.vite`
|
||||||
|
|
||||||
|
### Fork QoL Patches (not in upstream)
|
||||||
|
|
||||||
|
These are local modifications in the fork's UI. If re-copying source, these must be re-applied:
|
||||||
|
|
||||||
|
1. **stderr_group** — amber accordion for MCP init noise in `RunTranscriptView.tsx`
|
||||||
|
2. **tool_group** — accordion for consecutive non-terminal tools (write, read, search, browser)
|
||||||
|
3. **Dashboard excerpt** — `LatestRunCard` strips markdown, shows first 3 lines/280 chars
|
||||||
|
|
||||||
|
### Plugin System
|
||||||
|
|
||||||
|
PR #2218 (`feat/external-adapter-phase1`) adds external adapter support. See root `AGENTS.md` for full details.
|
||||||
|
|
||||||
|
- Adapters can be loaded as external plugins via `~/.paperclip/adapter-plugins.json`
|
||||||
|
- The plugin-loader should have ZERO hardcoded adapter imports — pure dynamic loading
|
||||||
|
- `createServerAdapter()` must include ALL optional fields (especially `detectModel`)
|
||||||
|
- Built-in UI adapters can shadow external plugin parsers — remove built-in when fully externalizing
|
||||||
|
- Reference external adapters: Hermes (`@henkey/hermes-paperclip-adapter` or `file:`) and Droid (npm)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ export type {
|
||||||
AdapterModel,
|
AdapterModel,
|
||||||
HireApprovedPayload,
|
HireApprovedPayload,
|
||||||
HireApprovedHookResult,
|
HireApprovedHookResult,
|
||||||
|
ConfigFieldOption,
|
||||||
|
ConfigFieldSchema,
|
||||||
|
AdapterConfigSchema,
|
||||||
ServerAdapterModule,
|
ServerAdapterModule,
|
||||||
QuotaWindow,
|
QuotaWindow,
|
||||||
ProviderQuotaResult,
|
ProviderQuotaResult,
|
||||||
|
|
|
||||||
|
|
@ -261,6 +261,30 @@ export interface ProviderQuotaResult {
|
||||||
windows: QuotaWindow[];
|
windows: QuotaWindow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Adapter config schema — declarative UI config for external adapters
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface ConfigFieldOption {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigFieldSchema {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
type: "text" | "select" | "toggle" | "number" | "textarea";
|
||||||
|
options?: ConfigFieldOption[];
|
||||||
|
default?: unknown;
|
||||||
|
hint?: string;
|
||||||
|
required?: boolean;
|
||||||
|
group?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdapterConfigSchema {
|
||||||
|
fields: ConfigFieldSchema[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface ServerAdapterModule {
|
export interface ServerAdapterModule {
|
||||||
type: string;
|
type: string;
|
||||||
execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult>;
|
execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult>;
|
||||||
|
|
@ -293,6 +317,13 @@ export interface ServerAdapterModule {
|
||||||
* the adapter does not support detection or no config is found.
|
* the adapter does not support detection or no config is found.
|
||||||
*/
|
*/
|
||||||
detectModel?: () => Promise<{ model: string; provider: string; source: string; candidates?: string[] } | null>;
|
detectModel?: () => Promise<{ model: string; provider: string; source: string; candidates?: string[] } | null>;
|
||||||
|
/**
|
||||||
|
* Optional: return a declarative config schema so the UI can render
|
||||||
|
* adapter-specific form fields without shipping React components.
|
||||||
|
* Dynamic options (e.g. scanning a profiles directory) should be
|
||||||
|
* resolved inside this method — the caller receives a fully hydrated schema.
|
||||||
|
*/
|
||||||
|
getConfigSchema?: () => Promise<AdapterConfigSchema> | AdapterConfigSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -353,4 +384,6 @@ export interface CreateConfigValues {
|
||||||
maxTurnsPerRun: number;
|
maxTurnsPerRun: number;
|
||||||
heartbeatEnabled: boolean;
|
heartbeatEnabled: boolean;
|
||||||
intervalSec: number;
|
intervalSec: number;
|
||||||
|
/** Arbitrary key-value pairs populated by schema-driven config fields. */
|
||||||
|
adapterSchemaValues?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,5 +25,8 @@ export type {
|
||||||
NativeContextManagement,
|
NativeContextManagement,
|
||||||
ResolvedSessionCompactionPolicy,
|
ResolvedSessionCompactionPolicy,
|
||||||
SessionCompactionPolicy,
|
SessionCompactionPolicy,
|
||||||
|
ConfigFieldOption,
|
||||||
|
ConfigFieldSchema,
|
||||||
|
AdapterConfigSchema,
|
||||||
ServerAdapterModule,
|
ServerAdapterModule,
|
||||||
} from "@paperclipai/adapter-utils";
|
} from "@paperclipai/adapter-utils";
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ import {
|
||||||
setAdapterDisabled,
|
setAdapterDisabled,
|
||||||
} from "../services/adapter-plugin-store.js";
|
} from "../services/adapter-plugin-store.js";
|
||||||
import type { AdapterPluginRecord } from "../services/adapter-plugin-store.js";
|
import type { AdapterPluginRecord } from "../services/adapter-plugin-store.js";
|
||||||
import type { ServerAdapterModule } from "../adapters/types.js";
|
import type { ServerAdapterModule, AdapterConfigSchema } from "../adapters/types.js";
|
||||||
import { loadExternalAdapterPackage, getUiParserSource, getOrExtractUiParserSource, reloadExternalAdapter } from "../adapters/plugin-loader.js";
|
import { loadExternalAdapterPackage, getUiParserSource, getOrExtractUiParserSource, reloadExternalAdapter } from "../adapters/plugin-loader.js";
|
||||||
import { logger } from "../middleware/logger.js";
|
import { logger } from "../middleware/logger.js";
|
||||||
import { assertBoard } from "./authz.js";
|
import { assertBoard } from "./authz.js";
|
||||||
|
|
@ -453,6 +453,7 @@ export function adapterRoutes() {
|
||||||
// Swap in the reloaded module
|
// Swap in the reloaded module
|
||||||
unregisterServerAdapter(type);
|
unregisterServerAdapter(type);
|
||||||
registerWithSessionManagement(newModule);
|
registerWithSessionManagement(newModule);
|
||||||
|
configSchemaCache.delete(type);
|
||||||
|
|
||||||
// Sync store.version from package.json (store may be missing version for local installs).
|
// Sync store.version from package.json (store may be missing version for local installs).
|
||||||
const record = getAdapterPluginByType(type);
|
const record = getAdapterPluginByType(type);
|
||||||
|
|
@ -520,6 +521,7 @@ export function adapterRoutes() {
|
||||||
|
|
||||||
unregisterServerAdapter(type);
|
unregisterServerAdapter(type);
|
||||||
registerWithSessionManagement(newModule);
|
registerWithSessionManagement(newModule);
|
||||||
|
configSchemaCache.delete(type);
|
||||||
|
|
||||||
// Sync store version from disk
|
// Sync store version from disk
|
||||||
let newVersion: string | undefined;
|
let newVersion: string | undefined;
|
||||||
|
|
@ -541,6 +543,44 @@ export function adapterRoutes() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── GET /api/adapters/:type/config-schema ────────────────────────────────
|
||||||
|
// Serve a declarative config schema for an adapter's UI form fields.
|
||||||
|
// The adapter's getConfigSchema() resolves all options (static and dynamic)
|
||||||
|
// so the UI receives a fully hydrated schema in a single fetch.
|
||||||
|
const configSchemaCache = new Map<string, { schema: AdapterConfigSchema; fetchedAt: number }>();
|
||||||
|
const CONFIG_SCHEMA_TTL_MS = 30_000;
|
||||||
|
|
||||||
|
router.get("/adapters/:type/config-schema", async (req, res) => {
|
||||||
|
assertBoard(req);
|
||||||
|
const { type } = req.params;
|
||||||
|
|
||||||
|
const adapter = findServerAdapter(type);
|
||||||
|
if (!adapter) {
|
||||||
|
res.status(404).json({ error: `Adapter "${type}" is not registered.` });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!adapter.getConfigSchema) {
|
||||||
|
res.status(404).json({ error: `Adapter "${type}" does not provide a config schema.` });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = configSchemaCache.get(type);
|
||||||
|
if (cached && Date.now() - cached.fetchedAt < CONFIG_SCHEMA_TTL_MS) {
|
||||||
|
res.json(cached.schema);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const schema = await adapter.getConfigSchema();
|
||||||
|
configSchemaCache.set(type, { schema, fetchedAt: Date.now() });
|
||||||
|
res.json(schema);
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
logger.error({ err, type }, "Failed to resolve config schema");
|
||||||
|
res.status(500).json({ error: `Failed to resolve config schema: ${message}` });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ── GET /api/adapters/:type/ui-parser.js ─────────────────────────────────
|
// ── GET /api/adapters/:type/ui-parser.js ─────────────────────────────────
|
||||||
// Serve the self-contained UI parser JS for an adapter type.
|
// Serve the self-contained UI parser JS for an adapter type.
|
||||||
// This allows external adapters to provide custom run-log parsing
|
// This allows external adapters to provide custom run-log parsing
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { hermesLocalUIAdapter } from "./hermes-local";
|
||||||
import { processUIAdapter } from "./process";
|
import { processUIAdapter } from "./process";
|
||||||
import { httpUIAdapter } from "./http";
|
import { httpUIAdapter } from "./http";
|
||||||
import { loadDynamicParser } from "./dynamic-loader";
|
import { loadDynamicParser } from "./dynamic-loader";
|
||||||
|
import { SchemaConfigFields, buildSchemaAdapterConfig } from "./schema-config-fields";
|
||||||
|
|
||||||
const uiAdapters: UIAdapterModule[] = [];
|
const uiAdapters: UIAdapterModule[] = [];
|
||||||
const adaptersByType = new Map<string, UIAdapterModule>();
|
const adaptersByType = new Map<string, UIAdapterModule>();
|
||||||
|
|
@ -60,7 +61,6 @@ export function getUIAdapter(type: string): UIAdapterModule {
|
||||||
const builtIn = adaptersByType.get(type);
|
const builtIn = adaptersByType.get(type);
|
||||||
|
|
||||||
if (!builtIn) {
|
if (!builtIn) {
|
||||||
// No built-in adapter — fall through to the external-only path.
|
|
||||||
let loadStarted = false;
|
let loadStarted = false;
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
|
|
@ -74,16 +74,16 @@ export function getUIAdapter(type: string): UIAdapterModule {
|
||||||
type,
|
type,
|
||||||
label: type,
|
label: type,
|
||||||
parseStdoutLine: parser,
|
parseStdoutLine: parser,
|
||||||
ConfigFields: processUIAdapter.ConfigFields,
|
ConfigFields: SchemaConfigFields,
|
||||||
buildAdapterConfig: processUIAdapter.buildAdapterConfig,
|
buildAdapterConfig: buildSchemaAdapterConfig,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return processUIAdapter.parseStdoutLine(line, ts);
|
return processUIAdapter.parseStdoutLine(line, ts);
|
||||||
},
|
},
|
||||||
ConfigFields: processUIAdapter.ConfigFields,
|
ConfigFields: SchemaConfigFields,
|
||||||
buildAdapterConfig: processUIAdapter.buildAdapterConfig,
|
buildAdapterConfig: buildSchemaAdapterConfig,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,16 +117,16 @@ export function syncExternalAdapters(
|
||||||
type,
|
type,
|
||||||
label,
|
label,
|
||||||
parseStdoutLine: parser,
|
parseStdoutLine: parser,
|
||||||
ConfigFields: processUIAdapter.ConfigFields,
|
ConfigFields: SchemaConfigFields,
|
||||||
buildAdapterConfig: processUIAdapter.buildAdapterConfig,
|
buildAdapterConfig: buildSchemaAdapterConfig,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return processUIAdapter.parseStdoutLine(line, ts);
|
return processUIAdapter.parseStdoutLine(line, ts);
|
||||||
},
|
},
|
||||||
ConfigFields: processUIAdapter.ConfigFields,
|
ConfigFields: SchemaConfigFields,
|
||||||
buildAdapterConfig: processUIAdapter.buildAdapterConfig,
|
buildAdapterConfig: buildSchemaAdapterConfig,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
259
ui/src/adapters/schema-config-fields.tsx
Normal file
259
ui/src/adapters/schema-config-fields.tsx
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
import type { AdapterConfigSchema, ConfigFieldSchema, CreateConfigValues } from "@paperclipai/adapter-utils";
|
||||||
|
|
||||||
|
import type { AdapterConfigFieldsProps } from "./types";
|
||||||
|
import {
|
||||||
|
Field,
|
||||||
|
DraftInput,
|
||||||
|
DraftNumberInput,
|
||||||
|
DraftTextarea,
|
||||||
|
ToggleField,
|
||||||
|
} from "../components/agent-config-primitives";
|
||||||
|
|
||||||
|
const inputClass =
|
||||||
|
"w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40";
|
||||||
|
|
||||||
|
const selectClass =
|
||||||
|
"w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Schema cache (module-level, survives re-renders)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const schemaCache = new Map<string, AdapterConfigSchema | null>();
|
||||||
|
const schemaFetchInflight = new Map<string, Promise<AdapterConfigSchema | null>>();
|
||||||
|
const failedSchemaTypes = new Set<string>();
|
||||||
|
|
||||||
|
async function fetchConfigSchema(adapterType: string): Promise<AdapterConfigSchema | null> {
|
||||||
|
const cached = schemaCache.get(adapterType);
|
||||||
|
if (cached !== undefined) return cached;
|
||||||
|
if (failedSchemaTypes.has(adapterType)) return null;
|
||||||
|
|
||||||
|
const inflight = schemaFetchInflight.get(adapterType);
|
||||||
|
if (inflight) return inflight;
|
||||||
|
|
||||||
|
const promise = (async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/adapters/${encodeURIComponent(adapterType)}/config-schema`);
|
||||||
|
if (!res.ok) {
|
||||||
|
failedSchemaTypes.add(adapterType);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const schema = (await res.json()) as AdapterConfigSchema;
|
||||||
|
schemaCache.set(adapterType, schema);
|
||||||
|
return schema;
|
||||||
|
} catch {
|
||||||
|
failedSchemaTypes.add(adapterType);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
schemaFetchInflight.delete(adapterType);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
schemaFetchInflight.set(adapterType, promise);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function invalidateConfigSchemaCache(adapterType: string): void {
|
||||||
|
schemaCache.delete(adapterType);
|
||||||
|
failedSchemaTypes.delete(adapterType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Hook
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function useConfigSchema(adapterType: string): AdapterConfigSchema | null {
|
||||||
|
const [schema, setSchema] = useState<AdapterConfigSchema | null>(
|
||||||
|
schemaCache.get(adapterType) ?? null,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
fetchConfigSchema(adapterType).then((s) => {
|
||||||
|
if (!cancelled) setSchema(s);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [adapterType]);
|
||||||
|
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function getDefaultValue(field: ConfigFieldSchema): unknown {
|
||||||
|
if (field.default !== undefined) return field.default;
|
||||||
|
switch (field.type) {
|
||||||
|
case "toggle":
|
||||||
|
return false;
|
||||||
|
case "number":
|
||||||
|
return 0;
|
||||||
|
case "text":
|
||||||
|
case "textarea":
|
||||||
|
return "";
|
||||||
|
case "select":
|
||||||
|
return field.options?.[0]?.value ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Component
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function SchemaConfigFields({
|
||||||
|
adapterType,
|
||||||
|
isCreate,
|
||||||
|
values,
|
||||||
|
set,
|
||||||
|
config,
|
||||||
|
eff,
|
||||||
|
mark,
|
||||||
|
}: AdapterConfigFieldsProps) {
|
||||||
|
const schema = useConfigSchema(adapterType);
|
||||||
|
|
||||||
|
const [defaultsApplied, setDefaultsApplied] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!schema || !isCreate || defaultsApplied) return;
|
||||||
|
const defaults: Record<string, unknown> = {};
|
||||||
|
for (const field of schema.fields) {
|
||||||
|
const def = getDefaultValue(field);
|
||||||
|
if (def !== undefined && def !== "") {
|
||||||
|
defaults[field.key] = def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Object.keys(defaults).length > 0) {
|
||||||
|
set?.({
|
||||||
|
adapterSchemaValues: { ...values?.adapterSchemaValues, ...defaults },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setDefaultsApplied(true);
|
||||||
|
}, [schema, isCreate, defaultsApplied, set, values?.adapterSchemaValues]);
|
||||||
|
|
||||||
|
if (!schema || schema.fields.length === 0) return null;
|
||||||
|
|
||||||
|
function readValue(field: ConfigFieldSchema): unknown {
|
||||||
|
if (isCreate) {
|
||||||
|
return values?.adapterSchemaValues?.[field.key] ?? getDefaultValue(field);
|
||||||
|
}
|
||||||
|
const stored = config[field.key];
|
||||||
|
return eff("adapterConfig", field.key, (stored ?? getDefaultValue(field)) as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeValue(field: ConfigFieldSchema, value: unknown): void {
|
||||||
|
if (isCreate) {
|
||||||
|
set?.({
|
||||||
|
adapterSchemaValues: {
|
||||||
|
...values?.adapterSchemaValues,
|
||||||
|
[field.key]: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
mark("adapterConfig", field.key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{schema.fields.map((field) => {
|
||||||
|
switch (field.type) {
|
||||||
|
case "select":
|
||||||
|
return (
|
||||||
|
<Field key={field.key} label={field.label} hint={field.hint}>
|
||||||
|
<select
|
||||||
|
className={selectClass}
|
||||||
|
value={String(readValue(field) ?? "")}
|
||||||
|
onChange={(e) => writeValue(field, e.target.value)}
|
||||||
|
>
|
||||||
|
{field.options?.map((opt) => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "toggle":
|
||||||
|
return (
|
||||||
|
<ToggleField
|
||||||
|
key={field.key}
|
||||||
|
label={field.label}
|
||||||
|
hint={field.hint}
|
||||||
|
checked={readValue(field) === true}
|
||||||
|
onChange={(v) => writeValue(field, v)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "number":
|
||||||
|
return (
|
||||||
|
<Field key={field.key} label={field.label} hint={field.hint}>
|
||||||
|
<DraftNumberInput
|
||||||
|
value={Number(readValue(field) ?? 0)}
|
||||||
|
onCommit={(v) => writeValue(field, v)}
|
||||||
|
immediate
|
||||||
|
className={inputClass}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "textarea":
|
||||||
|
return (
|
||||||
|
<Field key={field.key} label={field.label} hint={field.hint}>
|
||||||
|
<DraftTextarea
|
||||||
|
value={String(readValue(field) ?? "")}
|
||||||
|
onCommit={(v) => writeValue(field, v || undefined)}
|
||||||
|
immediate
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "text":
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<Field key={field.key} label={field.label} hint={field.hint}>
|
||||||
|
<DraftInput
|
||||||
|
value={String(readValue(field) ?? "")}
|
||||||
|
onCommit={(v) => writeValue(field, v || undefined)}
|
||||||
|
immediate
|
||||||
|
className={inputClass}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Build adapter config from schema values + standard CreateConfigValues fields
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function buildSchemaAdapterConfig(
|
||||||
|
values: CreateConfigValues,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const ac: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (values.model?.trim()) ac.model = values.model.trim();
|
||||||
|
if (values.cwd) ac.cwd = values.cwd;
|
||||||
|
if (values.command) ac.command = values.command;
|
||||||
|
if (values.instructionsFilePath) ac.instructionsFilePath = values.instructionsFilePath;
|
||||||
|
if (values.thinkingEffort) ac.thinkingEffort = values.thinkingEffort;
|
||||||
|
|
||||||
|
if (values.extraArgs) {
|
||||||
|
ac.extraArgs = values.extraArgs
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.adapterSchemaValues) {
|
||||||
|
Object.assign(ac, values.adapterSchemaValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ac;
|
||||||
|
}
|
||||||
|
|
@ -31,6 +31,7 @@ import { useToast } from "@/context/ToastContext";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ChoosePathButton } from "@/components/PathInstructionsModal";
|
import { ChoosePathButton } from "@/components/PathInstructionsModal";
|
||||||
import { invalidateDynamicParser } from "@/adapters/dynamic-loader";
|
import { invalidateDynamicParser } from "@/adapters/dynamic-loader";
|
||||||
|
import { invalidateConfigSchemaCache } from "@/adapters/schema-config-fields";
|
||||||
|
|
||||||
function AdapterRow({
|
function AdapterRow({
|
||||||
adapter,
|
adapter,
|
||||||
|
|
@ -215,6 +216,7 @@ export function AdapterManager() {
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
invalidate();
|
invalidate();
|
||||||
invalidateDynamicParser(result.type);
|
invalidateDynamicParser(result.type);
|
||||||
|
invalidateConfigSchemaCache(result.type);
|
||||||
pushToast({
|
pushToast({
|
||||||
title: "Adapter reloaded",
|
title: "Adapter reloaded",
|
||||||
body: `Type "${result.type}" reloaded.${result.version ? ` (v${result.version})` : ""}`,
|
body: `Type "${result.type}" reloaded.${result.version ? ` (v${result.version})` : ""}`,
|
||||||
|
|
@ -231,6 +233,7 @@ export function AdapterManager() {
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
invalidate();
|
invalidate();
|
||||||
invalidateDynamicParser(result.type);
|
invalidateDynamicParser(result.type);
|
||||||
|
invalidateConfigSchemaCache(result.type);
|
||||||
pushToast({
|
pushToast({
|
||||||
title: "Adapter reinstalled",
|
title: "Adapter reinstalled",
|
||||||
body: `Type "${result.type}" updated from npm.${result.version ? ` (v${result.version})` : ""}`,
|
body: `Type "${result.type}" updated from npm.${result.version ? ` (v${result.version})` : ""}`,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue