import { Icon } from "./Icon"; import { useState } from "react"; import { post } from "../api"; import { getLang, useT } from "../i18n"; import { navigate } from "../router"; import { useStore } from "../store"; import type { PermissionRequest, QuestionRequest } from "../types"; import { Diff, HunkDiff, splitHunks } from "./Diff"; function SubagentHint({ sessionId, currentId }: { sessionId: string; currentId: string }) { const t = useT(); const title = useStore((s) => s.sessionById[sessionId]?.title); if (sessionId === currentId) return null; return (
{t("interaction.fromSubagent")}{" "} (e.preventDefault(), navigate(`/s/${sessionId}`))}> {title || sessionId.slice(-6)}
); } export function PermissionCard({ req, currentId }: { req: PermissionRequest; currentId: string }) { const t = useT(); const [feedback, setFeedback] = useState(""); const [busy, setBusy] = useState(false); // File edits with several hunks can be accepted piece by piece. const hunkCount = req.diff && (req.tool === "edit" || req.tool === "write") ? splitHunks(req.diff).hunks.length : 0; const [selected, setSelected] = useState(() => Array(hunkCount).fill(true)); const chosen = selected.filter(Boolean).length; const partial = hunkCount > 1 && chosen < hunkCount; const [explaining, setExplaining] = useState(false); const [explanation, setExplanation] = useState(); const [explainError, setExplainError] = useState(); /** The small model explains in plain words what the action does (approving on a phone without reading code). */ const explain = async () => { setExplaining(true); setExplainError(undefined); try { const r = await post<{ text: string }>(`/permissions/${req.id}/explain`, { lang: getLang() }); setExplanation(r.text); } catch (e: any) { setExplainError(e.message); } finally { setExplaining(false); } }; const reply = async (r: "once" | "always" | "project" | "reject") => { setBusy(true); try { const hunks = r === "once" && partial ? selected.flatMap((on, i) => (on ? [i] : [])) : undefined; await post(`/permissions/${req.id}`, { reply: r, message: r === "reject" || partial ? feedback || undefined : undefined, hunks }); } finally { setBusy(false); } }; const command: string | undefined = req.tool === "bash" ? req.input?.command : undefined; const alwaysLabel = req.tool === "bash" ? t("permission.alwaysCommand", { prefix: (command ?? "").split(/\s+/).slice(0, 2).join(" ") }) : t("permission.alwaysTool", { tool: req.tool }); // Dangerous commands are only ever allowed for the session (the server decides; this hides the button). const dangerous = !!command && /\b(rm\s+-[a-zA-Z]*[rRf]|sudo|su|git\s+(push|reset|clean|rebase|checkout|restore)|docker|docker-compose|systemctl|chmod\s+-R|chown\s+-R|dd|mkfs|shutdown|reboot|kill|pkill|killall|crontab)\b|\|\s*(ba)?sh\b|publish/.test(command); const projectLabel = req.tool === "bash" ? t("permission.projectCommand", { prefix: (command ?? "").split(/\s+/).slice(0, 2).join(" ") }) : t("permission.projectTool", { tool: req.tool }); return (

{t("permission.title")} {req.tool}

{req.title}
{command &&
$ {command}
} {req.diff && hunkCount > 1 ? ( <>
{t("permission.hunksHint", { n: hunkCount })}
setSelected((s) => s.map((v, j) => (j === i ? !v : v)))} /> ) : req.diff ? ( ) : ( !command && (
{t("tool.input")}
              {JSON.stringify(req.input, null, 2)}
            
) )} {explanation ? (
{explanation}
) : ( )} {explainError &&
{explainError}
} setFeedback(e.target.value)} onKeyDown={(e) => e.key === "Enter" && reply("reject")} />
{!dangerous && ( )}
); } const OTHER = "__other__"; export function QuestionCard({ req, currentId }: { req: QuestionRequest; currentId: string }) { const t = useT(); const [selected, setSelected] = useState>({}); const [other, setOther] = useState>({}); const [busy, setBusy] = useState(false); const toggle = (qi: number, label: string, multi: boolean) => { setSelected((s) => { const cur = s[qi] ?? []; if (!multi) return { ...s, [qi]: [label] }; return { ...s, [qi]: cur.includes(label) ? cur.filter((x) => x !== label) : [...cur, label] }; }); }; const answerFor = (qi: number): string | string[] | undefined => { const q = req.questions[qi]; const sel = (selected[qi] ?? []).map((l) => (l === OTHER ? other[qi]?.trim() ?? "" : l)).filter(Boolean); if (q.options.length === 0) return other[qi]?.trim() || undefined; if (sel.length === 0) return undefined; return q.multi_select ? sel : sel[0]; }; const complete = req.questions.every((_, i) => answerFor(i) !== undefined); const submit = async (dismiss = false) => { setBusy(true); try { const answers = dismiss ? null : Object.fromEntries(req.questions.map((q, i) => [q.question, answerFor(i)])); await post(`/questions/${req.id}`, { answers }); } finally { setBusy(false); } }; const source = req.source.startsWith("mcp:") ? t("question.fromMcp", { server: req.source.slice(4) }) : t("question.fromAgent"); return (

{t("question.title")}

{source}
{req.questions.map((q, qi) => (
{q.header && {q.header}}
{q.question}
{q.options.length > 0 ? (
{[...q.options, { label: OTHER, description: "" }].map((o) => { const isSel = (selected[qi] ?? []).includes(o.label); return (
toggle(qi, o.label, !!q.multi_select)} onKeyDown={(e) => (e.key === " " || e.key === "Enter") && toggle(qi, o.label, !!q.multi_select)} >
{o.label === OTHER ? ( e.stopPropagation()} onFocus={() => !isSel && toggle(qi, OTHER, !!q.multi_select)} onChange={(e) => setOther((s) => ({ ...s, [qi]: e.target.value }))} /> ) : ( <>
{o.label}
{o.description &&
{o.description}
} )}
); })}
) : (