import { useState } from 'react'; import { ShieldAlert, MessageCircleQuestion } from 'lucide-react'; import type { PermissionPrompt } from '@/api/types'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; interface Props { prompt: PermissionPrompt; onRespond: (optionId: string | null, updatedInput?: Record) => void; busy?: boolean; } // --------------------------------------------------------------------------- // Question detection — ACP's RequestPermissionRequest carries the tool input // in `input`. Claude Code's AskUserQuestion puts { questions: [...] } there. // --------------------------------------------------------------------------- interface Question { question: string; header?: string; options: string[]; multiSelect: boolean; } function parseQuestions(input: Record | undefined): Question[] | null { if (!input) return null; const raw = input.questions; if (!Array.isArray(raw)) return null; const out: Question[] = []; for (const item of raw) { if (!item || typeof item !== 'object') continue; const q = item as { question?: unknown; header?: unknown; options?: unknown; multiSelect?: unknown }; if (typeof q.question !== 'string') continue; const opts = Array.isArray(q.options) ? q.options.filter((o): o is string => typeof o === 'string') : []; out.push({ question: q.question, header: typeof q.header === 'string' ? q.header : undefined, options: opts, multiSelect: q.multiSelect === true, }); } return out.length > 0 ? out : null; } // --------------------------------------------------------------------------- // Elicitation detection — ACP's createElicitation carries a JSON Schema in // `input.requestedSchema`. For now, render each property as a text input. // --------------------------------------------------------------------------- interface ElicitationField { key: string; title: string; description?: string; type: string; enumValues?: string[]; } function parseElicitation(input: Record | undefined): { message: string; fields: ElicitationField[] } | null { if (!input) return null; const schema = input.requestedSchema; if (!schema || typeof schema !== 'object') return null; const s = schema as Record; const props = s.properties; if (!props || typeof props !== 'object') return null; const fields: ElicitationField[] = []; for (const [key, val] of Object.entries(props as Record)) { if (!val || typeof val !== 'object') continue; const p = val as Record; fields.push({ key, title: typeof p.title === 'string' ? p.title : key, description: typeof p.description === 'string' ? p.description : undefined, type: typeof p.type === 'string' ? p.type : 'string', enumValues: Array.isArray(p.enum) ? p.enum.filter((e): e is string => typeof e === 'string') : undefined, }); } if (fields.length === 0) return null; return { message: typeof input.message === 'string' ? input.message : '', fields }; } export function PermissionCard({ prompt, onRespond, busy }: Props) { const isQuestion = prompt.kind === 'question'; const isElicitation = prompt.kind === 'elicitation'; if (isQuestion) { const questions = parseQuestions(prompt.input); if (questions) { return ; } } if (isElicitation) { const elicitation = parseElicitation(prompt.input); if (elicitation) { return ; } } // Standard tool permission — approve/deny buttons return (

Permission required

{prompt.toolTitle && (

{prompt.toolTitle}

)}
{prompt.options.map((opt) => ( ))}
); } // --------------------------------------------------------------------------- // QuestionView — renders Claude's AskUserQuestion as interactive radio/checkbox // --------------------------------------------------------------------------- function QuestionView({ questions, prompt, onRespond, busy, }: { questions: Question[]; prompt: PermissionPrompt; onRespond: Props['onRespond']; busy?: boolean; }) { const [selections, setSelections] = useState(() => questions.map(() => [])); const [freeTexts, setFreeTexts] = useState(() => questions.map(() => '')); const [submitting, setSubmitting] = useState(false); const disabled = busy || submitting; const allComplete = questions.every((_, i) => selections[i]!.length > 0 || freeTexts[i]!.trim().length > 0, ); function buildAnswers(): Record { const answers: Record = {}; for (let i = 0; i < questions.length; i++) { const q = questions[i]!; const key = q.question; const selected = selections[i]!; const free = freeTexts[i]!.trim(); if (free) { answers[key] = free; } else if (selected.length > 0) { answers[key] = selected.join(', '); } } return answers; } function handleSubmit() { if (!allComplete || submitting) return; setSubmitting(true); const answers = buildAnswers(); const firstAllow = prompt.options.find((o) => o.label.toLowerCase().includes('allow') || o.label.toLowerCase().includes('yes'), ); onRespond(firstAllow?.optionId ?? prompt.options[0]?.optionId ?? null, { ...prompt.input, answers, }); } function pickSingle(qIdx: number, option: string) { setSelections((prev) => prev.map((arr, i) => (i === qIdx ? [option] : arr))); if (questions.length === 1 && !freeTexts[0]!.trim()) { setSubmitting(true); const firstAllow = prompt.options.find((o) => o.label.toLowerCase().includes('allow') || o.label.toLowerCase().includes('yes'), ); onRespond(firstAllow?.optionId ?? prompt.options[0]?.optionId ?? null, { ...prompt.input, answers: { [questions[0]!.question]: option }, }); } } function toggleMulti(qIdx: number, option: string) { setSelections((prev) => prev.map((arr, i) => { if (i !== qIdx) return arr; return arr.includes(option) ? arr.filter((o) => o !== option) : [...arr, option]; }), ); } return (
{questions.map((q, i) => (
{questions.length > 1 && (
{q.header ?? `Question ${i + 1}`}
)}
{q.question}
{q.options.length > 0 && !q.multiSelect && ( pickSingle(i, v)} disabled={disabled} className="gap-1.5" > {q.options.map((opt, j) => { const id = `q${i}-opt${j}`; return ( ); })} )} {q.options.length > 0 && q.multiSelect && (
{q.options.map((opt, j) => { const id = `q${i}-opt${j}`; const checked = selections[i]!.includes(opt); return ( ); })}
)}
Or type a custom answer
setFreeTexts((prev) => prev.map((t, idx) => (idx === i ? e.target.value : t))) } className="w-full rounded border border-input bg-background px-2 py-1 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/40 disabled:opacity-60" />
))}
{(questions.length > 1 || freeTexts.some((t) => t.trim())) && (
)}
); } // --------------------------------------------------------------------------- // ElicitationView — renders ACP elicitation forms (JSON Schema-driven) // --------------------------------------------------------------------------- function ElicitationView({ elicitation, prompt, onRespond, busy, }: { elicitation: { message: string; fields: ElicitationField[] }; prompt: PermissionPrompt; onRespond: Props['onRespond']; busy?: boolean; }) { const [values, setValues] = useState>(() => { const init: Record = {}; for (const f of elicitation.fields) init[f.key] = ''; return init; }); const [submitting, setSubmitting] = useState(false); const disabled = busy || submitting; const allFilled = elicitation.fields.every((f) => (values[f.key] ?? '').trim().length > 0); function handleSubmit() { if (!allFilled || submitting) return; setSubmitting(true); const content: Record = {}; for (const f of elicitation.fields) { const raw = values[f.key]!.trim(); if (f.type === 'number' || f.type === 'integer') { content[f.key] = Number(raw); } else if (f.type === 'boolean') { content[f.key] = raw === 'true' || raw === 'yes' || raw === '1'; } else { content[f.key] = raw; } } const firstAllow = prompt.options[0]; onRespond(firstAllow?.optionId ?? null, content); } return (

{elicitation.message}

{elicitation.fields.map((f) => (
{f.description && (

{f.description}

)} {f.enumValues ? ( setValues((prev) => ({ ...prev, [f.key]: v }))} disabled={disabled} className="gap-1.5" > {f.enumValues.map((opt, j) => { const id = `e-${f.key}-${j}`; return ( ); })} ) : ( setValues((prev) => ({ ...prev, [f.key]: e.target.value }))} className="w-full rounded border border-input bg-background px-2 py-1 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/40 disabled:opacity-60" /> )}
))}
); }