Files
patchpass/UI/src/components/AgentModals.tsx
T
Space-Banane 7e05dd918c
Deploy / Build (push) Successful in 28s
Deploy / Test & Lint (push) Failing after 29s
Deploy / Build and Push Docker Image (push) Has been skipped
Patchpass V1
2026-07-18 20:14:44 +02:00

251 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { toast } from "react-toastify";
import { agents as agentsApi } from "../api/client";
import type { Agent } from "../api/types";
import { Modal } from "./Modal";
import { Button, Input, Textarea } from "./ui";
import { CodeBlock } from "./CodeBlock";
// ── Create / edit form ───────────────────────────────────────────────────────────
export function AgentFormModal({
open,
onClose,
agent,
onSaved,
}: {
open: boolean;
onClose: () => void;
agent?: Agent | null;
onSaved: (agent: Agent, created: boolean) => void;
}) {
const editing = !!agent;
const [name, setName] = useState(agent?.name ?? "");
const [description, setDescription] = useState(agent?.description ?? "");
const [website, setWebsite] = useState(agent?.website ?? "");
const [iconUrl, setIconUrl] = useState(agent?.icon_url ?? "");
const [maxPending, setMaxPending] = useState(agent?.max_pending_requests ?? 5);
const [loading, setLoading] = useState(false);
const submit = async () => {
if (!name.trim()) return toast.error("Name is required");
setLoading(true);
try {
const payload = {
name: name.trim(),
description: description.trim() || null,
website: website.trim() || null,
icon_url: iconUrl.trim() || null,
max_pending_requests: maxPending,
};
const saved = editing
? await agentsApi.update(agent!.id, payload)
: await agentsApi.create(payload);
toast.success(editing ? "Agent updated" : "Agent created");
onSaved(saved, !editing);
onClose();
} catch (err) {
toast.error(err instanceof Error ? err.message : "Failed");
} finally {
setLoading(false);
}
};
return (
<Modal
open={open}
onClose={onClose}
title={editing ? "Edit agent" : "New agent"}
footer={
<>
<Button variant="ghost" onClick={onClose}>
Cancel
</Button>
<Button onClick={submit} loading={loading}>
{editing ? "Save" : "Create agent"}
</Button>
</>
}
>
<div className="space-y-4">
<Input label="Name" value={name} onChange={(e) => setName(e.target.value)} autoFocus />
<Textarea
label="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={2}
/>
<Input
label="Website (optional)"
value={website}
onChange={(e) => setWebsite(e.target.value)}
placeholder="https://…"
/>
<Input
label="Icon URL (optional)"
value={iconUrl}
onChange={(e) => setIconUrl(e.target.value)}
placeholder="https://…/icon.png"
hint="Verified on save. Must be a JPG, PNG, or GIF under 1 MB."
/>
<div>
<div className="mb-1 flex items-center justify-between text-sm">
<span className="text-muted">Max pending requests</span>
<span className="font-medium text-text">{maxPending}</span>
</div>
<input
type="range"
min={1}
max={10}
value={maxPending}
onChange={(e) => setMaxPending(Number(e.target.value))}
className="w-full accent-[var(--color-primary)]"
/>
<p className="mt-1 text-xs text-faint">
How many requests this agent may have awaiting review at once (110).
</p>
</div>
</div>
</Modal>
);
}
// ── Connect (config + key) ─────────────────────────────────────────────────────────
type Tab = "openclaw" | "mcp" | "rest";
export function ConnectModal({
open,
onClose,
agent,
revealedKey,
}: {
open: boolean;
onClose: () => void;
agent: Agent;
revealedKey?: string | null;
}) {
const [tab, setTab] = useState<Tab>("openclaw");
const origin = window.location.origin;
const key = revealedKey ?? "YOUR_API_KEY";
const hasKey = !!revealedKey;
const openclawConfig = JSON.stringify(
{
mcp: {
servers: {
patchpass: {
transport: "streamable-http",
url: `${origin}/mcp`,
headers: { "x-api-key": key },
},
},
},
},
null,
2,
);
const mcpConfig = JSON.stringify(
{
mcpServers: {
patchpass: {
type: "streamable-http",
url: `${origin}/mcp`,
headers: { "x-api-key": key },
},
},
},
null,
2,
);
const restExample = `# Submit a change request
curl -X POST ${origin}/v1/change-requests \\
-H "x-api-key: ${key}" \\
-H "Content-Type: application/json" \\
-d '{
"title": "Update deployment",
"changes": [
{ "type": "config", "path": "timeout", "before": 30, "after": 60, "content_type": "integer" }
]
}'
# Poll for the decision
curl ${origin}/v1/change-requests/{request_id} -H "x-api-key: ${key}"
# Consume the approval before proceeding
curl -X POST ${origin}/v1/change-requests/{request_id}/consume -H "x-api-key: ${key}"`;
const tabs: { id: Tab; label: string }[] = [
{ id: "openclaw", label: "OpenClaw" },
{ id: "mcp", label: "MCP (generic)" },
{ id: "rest", label: "REST API" },
];
return (
<Modal open={open} onClose={onClose} title={`Connect "${agent.name}"`} wide footer={<Button onClick={onClose}>Done</Button>}>
<div className="space-y-4">
{hasKey ? (
<div className="rounded-lg border border-pending/40 bg-pending/10 p-3">
<p className="text-xs font-semibold text-pending">Save this API key now it won't be shown again.</p>
<div className="mt-2">
<CodeBlock code={revealedKey!} />
</div>
</div>
) : (
<p className="text-sm text-muted">
The API key is only shown once, at creation or after regeneration. Configs below use a
placeholder substitute your saved key.
</p>
)}
<div className="flex gap-1 border-b border-border">
{tabs.map((t) => (
<button
key={t.id}
onClick={() => setTab(t.id)}
className={`px-3 py-2 text-sm font-medium transition ${
tab === t.id ? "border-b-2 border-primary text-text" : "text-muted hover:text-text"
}`}
>
{t.label}
</button>
))}
</div>
{tab === "openclaw" && (
<div className="space-y-2">
<p className="text-sm text-muted">
Add this to <code className="text-text">~/.openclaw/openclaw.json</code>, then tell your
agent to request approval via PatchPass before taking action.
</p>
<CodeBlock code={openclawConfig} language="json" />
</div>
)}
{tab === "mcp" && (
<div className="space-y-2">
<p className="text-sm text-muted">
Generic streamable-HTTP MCP client config (Claude Desktop, Cursor, etc.).
</p>
<CodeBlock code={mcpConfig} language="json" />
</div>
)}
{tab === "rest" && (
<div className="space-y-2">
<p className="text-sm text-muted">Or call the REST API directly with the agent key.</p>
<CodeBlock code={restExample} language="bash" />
</div>
)}
<div className="rounded-lg border border-border bg-surface-raised/40 p-3 text-xs text-muted">
<p className="mb-1 font-semibold text-text">Recommended agent instruction</p>
Before taking any consequential action (deploys, config changes, code edits), call
<code className="mx-1 text-text">create_request</code>, wait for approval, then
<code className="mx-1 text-text">consume_approval</code> before proceeding.
</div>
</div>
</Modal>
);
}