Files
paperclip/ui/src/adapters/openclaw-gateway/config-fields.tsx
2026-03-10 10:58:38 -05:00

238 lines
7.2 KiB
TypeScript

import { useState } from "react";
import { Eye, EyeOff } from "lucide-react";
import type { AdapterConfigFieldsProps } from "../types";
import {
Field,
DraftInput,
help,
} from "../../components/agent-config-primitives";
import {
PayloadTemplateJsonField,
RuntimeServicesJsonField,
} from "../runtime-json-fields";
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";
function SecretField({
label,
value,
onCommit,
placeholder,
}: {
label: string;
value: string;
onCommit: (v: string) => void;
placeholder?: string;
}) {
const [visible, setVisible] = useState(false);
return (
<Field label={label}>
<div className="relative">
<button
type="button"
onClick={() => setVisible((v) => !v)}
className="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground/50 hover:text-muted-foreground transition-colors"
>
{visible ? <Eye className="h-3.5 w-3.5" /> : <EyeOff className="h-3.5 w-3.5" />}
</button>
<DraftInput
value={value}
onCommit={onCommit}
immediate
type={visible ? "text" : "password"}
className={inputClass + " pl-8"}
placeholder={placeholder}
/>
</div>
</Field>
);
}
function parseScopes(value: unknown): string {
if (Array.isArray(value)) {
return value.filter((entry): entry is string => typeof entry === "string").join(", ");
}
return typeof value === "string" ? value : "";
}
export function OpenClawGatewayConfigFields({
isCreate,
values,
set,
config,
eff,
mark,
}: AdapterConfigFieldsProps) {
const configuredHeaders =
config.headers && typeof config.headers === "object" && !Array.isArray(config.headers)
? (config.headers as Record<string, unknown>)
: {};
const effectiveHeaders =
(eff("adapterConfig", "headers", configuredHeaders) as Record<string, unknown>) ?? {};
const effectiveGatewayToken = typeof effectiveHeaders["x-openclaw-token"] === "string"
? String(effectiveHeaders["x-openclaw-token"])
: typeof effectiveHeaders["x-openclaw-auth"] === "string"
? String(effectiveHeaders["x-openclaw-auth"])
: "";
const commitGatewayToken = (rawValue: string) => {
const nextValue = rawValue.trim();
const nextHeaders: Record<string, unknown> = { ...effectiveHeaders };
if (nextValue) {
nextHeaders["x-openclaw-token"] = nextValue;
delete nextHeaders["x-openclaw-auth"];
} else {
delete nextHeaders["x-openclaw-token"];
delete nextHeaders["x-openclaw-auth"];
}
mark("adapterConfig", "headers", Object.keys(nextHeaders).length > 0 ? nextHeaders : undefined);
};
const sessionStrategy = eff(
"adapterConfig",
"sessionKeyStrategy",
String(config.sessionKeyStrategy ?? "fixed"),
);
return (
<>
<Field label="Gateway URL" hint={help.webhookUrl}>
<DraftInput
value={
isCreate
? values!.url
: eff("adapterConfig", "url", String(config.url ?? ""))
}
onCommit={(v) =>
isCreate
? set!({ url: v })
: mark("adapterConfig", "url", v || undefined)
}
immediate
className={inputClass}
placeholder="ws://127.0.0.1:18789"
/>
</Field>
<PayloadTemplateJsonField
isCreate={isCreate}
values={values}
set={set}
config={config}
mark={mark}
/>
<RuntimeServicesJsonField
isCreate={isCreate}
values={values}
set={set}
config={config}
mark={mark}
/>
{!isCreate && (
<>
<Field label="Paperclip API URL override">
<DraftInput
value={
eff(
"adapterConfig",
"paperclipApiUrl",
String(config.paperclipApiUrl ?? ""),
)
}
onCommit={(v) => mark("adapterConfig", "paperclipApiUrl", v || undefined)}
immediate
className={inputClass}
placeholder="https://paperclip.example"
/>
</Field>
<Field label="Session strategy">
<select
value={sessionStrategy}
onChange={(e) => mark("adapterConfig", "sessionKeyStrategy", e.target.value)}
className={inputClass}
>
<option value="fixed">Fixed</option>
<option value="issue">Per issue</option>
<option value="run">Per run</option>
</select>
</Field>
{sessionStrategy === "fixed" && (
<Field label="Session key">
<DraftInput
value={eff("adapterConfig", "sessionKey", String(config.sessionKey ?? "paperclip"))}
onCommit={(v) => mark("adapterConfig", "sessionKey", v || undefined)}
immediate
className={inputClass}
placeholder="paperclip"
/>
</Field>
)}
<SecretField
label="Gateway auth token (x-openclaw-token)"
value={effectiveGatewayToken}
onCommit={commitGatewayToken}
placeholder="OpenClaw gateway token"
/>
<Field label="Role">
<DraftInput
value={eff("adapterConfig", "role", String(config.role ?? "operator"))}
onCommit={(v) => mark("adapterConfig", "role", v || undefined)}
immediate
className={inputClass}
placeholder="operator"
/>
</Field>
<Field label="Scopes (comma-separated)">
<DraftInput
value={eff("adapterConfig", "scopes", parseScopes(config.scopes ?? ["operator.admin"]))}
onCommit={(v) => {
const parsed = v
.split(",")
.map((entry) => entry.trim())
.filter(Boolean);
mark("adapterConfig", "scopes", parsed.length > 0 ? parsed : undefined);
}}
immediate
className={inputClass}
placeholder="operator.admin"
/>
</Field>
<Field label="Wait timeout (ms)">
<DraftInput
value={eff("adapterConfig", "waitTimeoutMs", String(config.waitTimeoutMs ?? "120000"))}
onCommit={(v) => {
const parsed = Number.parseInt(v.trim(), 10);
mark(
"adapterConfig",
"waitTimeoutMs",
Number.isFinite(parsed) && parsed > 0 ? parsed : undefined,
);
}}
immediate
className={inputClass}
placeholder="120000"
/>
</Field>
<Field label="Device auth">
<div className="text-xs text-muted-foreground leading-relaxed">
Always enabled for gateway agents. Paperclip persists a device key during onboarding so pairing approvals
remain stable across runs.
</div>
</Field>
</>
)}
</>
);
}