import { useMemo, useState } from 'react'; import { Check, Loader2, Sparkles, Wand2, X } from 'lucide-react'; import { useHeadscalePolicyAssist, assistFailure } from './useHeadscalePolicy'; import { collapseUnchanged, diffCounts, diffLines } from './diff'; import { Button, Card, ErrorNote } from './Cards'; // Ask for a policy change in English; read the diff; decide. // // The whole point of this panel is the middle step. The model is good at the grammar — HuJSON, tagOwners, // the src/dst shapes — and has no idea which of the owner's machines matter, so its proposal is a draft to // be read, not an answer to be trusted. Nothing here writes to Headscale: Apply puts the text in the editor // above and the existing Save button is still the only thing that leaves the browser. // // The diff is against what is CURRENTLY in the editor, which is also what was sent up, so it always shows // exactly what accepting would change on screen — including edits the owner made and hasn't saved. const EXAMPLES = [ 'let everyone reach the machines tagged tag:server on port 22', 'stop the phones from reaching anything except the DNS server', 'add a group for family with just my own user in it', ]; const DiffBody = ({ before, after }: { before: string; after: string }) => { const lines = useMemo(() => diffLines(before, after), [before, after]); const rows = useMemo(() => collapseUnchanged(lines), [lines]); const { added, removed } = useMemo(() => diffCounts(lines), [lines]); if (!added && !removed) { return

No change — the proposal matches what you have.

; } return ( <>
+{added} −{removed} unchanged lines collapsed
{rows.map((row, index) => row === null ? (
) : (
{row.kind === 'add' ? '+' : row.kind === 'remove' ? '−' : ' '} {row.text}
), )}
); }; type PolicyAssistantProps = { /** The text on screen right now. Sent up as the base, and diffed against. */ policy: string; /** Accepting a proposal — puts it in the editor's draft. Never saves. */ onApply: (policy: string) => void; disabled?: boolean; }; export const PolicyAssistant = ({ policy, onApply, disabled }: PolicyAssistantProps) => { const assist = useHeadscalePolicyAssist(); const [prompt, setPrompt] = useState(''); // The proposal lives here, not in the mutation's `data`, so that discarding it is a real state change and // a second ask doesn't briefly show the previous answer against the new base. const [proposal, setProposal] = useState<{ explanation: string; policy: string } | null>(null); const ask = async () => { const request = prompt.trim(); if (!request || assist.isPending) return; setProposal(null); try { setProposal(await assist.mutateAsync({ prompt: request, policy })); } catch { // Rendered from `assist.error` below — mutateAsync rejecting is the same failure twice. } }; const apply = () => { if (!proposal) return; onApply(proposal.policy); setProposal(null); setPrompt(''); assist.reset(); }; return (
Describe the change Proposes a document — never saves it