New core product layout: resizable chat + artifacts panel replaces the old wizard-only flow. Front door (create/grow), onboarding exits to chat, CEO discusses strategy before planning. Approval actions live in the artifacts pane, not inline in chat. Chat history drawer, animated paperclip thinking indicator, optimistic typing, faster polling. Rename Issue → Task across all frontend UI labels (16 files). Add global pause/resume all agents on dashboard with sidebar badge. Move toasts to bottom-right. Add Artifacts page and sidebar nav item. Reorder wizard: Mission → CEO config → Launch (exits to chat). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
237 lines
7.2 KiB
TypeScript
237 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 task</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>
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
}
|