Files
patchpass/UI/src/components/DecisionModal.tsx
T
luna dfa0c55e3c
Deploy / Build (pull_request) Successful in 29s
Deploy / Test & Lint (pull_request) Successful in 32s
Deploy / Build and Push Docker Image (pull_request) Has been skipped
feat: prefill agent-edit modal, Ctrl+Enter submits modals, improve README
- AgentFormModal: sync form state to current agent values when the modal
  opens (useEffect on open/agent), so edit always shows the agent's
  current name, description, website, icon URL, and max-pending value.
- Modal: add optional onSubmit prop; Ctrl+Enter (or ⌘+Enter) fires it,
  covering AgentFormModal, DecisionModal, and both Settings modals
  (disable 2FA, delete account) without breaking textarea newlines or
  text-input behaviour.
- README: substantially expanded — purpose, capabilities, architecture,
  quick-start with Docker Compose, dev setup, testing, agent integration
  (OpenClaw, generic MCP, REST), and privacy notes.
- TODO.md: remove three completed items (prefill modal, Ctrl+Enter, human
  readme).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-19 20:39:43 +00:00

128 lines
3.8 KiB
TypeScript

import { useEffect, useState } from "react";
import { toast } from "react-toastify";
import { requests } from "../api/client";
import type { ChangeRequest } from "../api/types";
import { Modal } from "./Modal";
import { Button } from "./ui";
type Decision = "APPROVE" | "REJECT" | "REQUEST_CHANGES";
const MAX = 500;
const meta: Record<Decision, { title: string; verb: string; variant: "success" | "danger" | "primary"; blurb: string; commentRequired: boolean }> = {
APPROVE: {
title: "Approve request",
verb: "Approve",
variant: "success",
blurb: "The agent will be allowed to proceed. A platform-signed receipt will be issued.",
commentRequired: false,
},
REJECT: {
title: "Reject request",
verb: "Reject",
variant: "danger",
blurb: "This is a hard blocker. The agent must submit a new request to try again.",
commentRequired: false,
},
REQUEST_CHANGES: {
title: "Request changes",
verb: "Request changes",
variant: "primary",
blurb: "The agent will see your notes and can update the request for re-review.",
commentRequired: true,
},
};
export function DecisionModal({
request,
decision,
onClose,
onDone,
}: {
request: ChangeRequest;
decision: Decision | null;
onClose: () => void;
onDone: (updated: ChangeRequest) => void;
}) {
const [comment, setComment] = useState("");
const [loading, setLoading] = useState(false);
useEffect(() => {
setComment("");
}, [decision]);
if (!decision) return null;
const m = meta[decision];
const tooLong = comment.length > MAX;
const missingRequired = m.commentRequired && comment.trim().length === 0;
const submit = async () => {
if (tooLong || missingRequired) return;
setLoading(true);
try {
const updated = await requests.decide(request.request_id, decision, comment.trim() || undefined);
toast.success(`${m.verb}d`);
onDone(updated);
onClose();
} catch (err) {
toast.error(err instanceof Error ? err.message : "Failed");
} finally {
setLoading(false);
}
};
return (
<Modal
open={!!decision}
onClose={onClose}
onSubmit={submit}
title={m.title}
footer={
<>
<Button variant="ghost" onClick={onClose}>
Cancel
</Button>
<Button variant={m.variant} onClick={submit} loading={loading} disabled={tooLong || missingRequired}>
{m.verb}
</Button>
</>
}
>
<div className="space-y-4">
<div className="rounded-lg border border-border bg-surface-raised/50 p-3">
<p className="text-sm font-medium text-text">{request.title}</p>
<p className="mt-1 text-xs text-muted">
{request.changes.length} change{request.changes.length === 1 ? "" : "s"} ·{" "}
{request.agent?.name}
</p>
</div>
<p className="text-sm text-muted">{m.blurb}</p>
<div>
<div className="mb-1 flex items-center justify-between">
<span className="text-sm text-muted">
Comment {m.commentRequired ? <span className="text-changes">(required)</span> : "(optional)"}
</span>
<span className={`text-xs ${tooLong ? "text-rejected" : comment.length > MAX * 0.8 ? "text-pending" : "text-faint"}`}>
{comment.length}/{MAX}
</span>
</div>
<textarea
value={comment}
onChange={(e) => setComment(e.target.value)}
rows={4}
autoFocus
placeholder={m.commentRequired ? "Explain what needs to change…" : "Add an optional note…"}
className={`w-full rounded-lg border bg-bg px-3 py-2 text-sm text-text outline-none focus:border-primary ${
tooLong ? "border-rejected" : "border-border-strong"
}`}
/>
{tooLong && <p className="mt-1 text-xs text-rejected">Comment is too long (max {MAX}).</p>}
{missingRequired && (
<p className="mt-1 text-xs text-changes">A comment is required when requesting changes.</p>
)}
</div>
</div>
</Modal>
);
}