diff --git a/web/oss/src/components/AgentChatSlice/components/ApprovalDock.tsx b/web/oss/src/components/AgentChatSlice/components/ApprovalDock.tsx index 97d5ab3126..0d80963d1d 100644 --- a/web/oss/src/components/AgentChatSlice/components/ApprovalDock.tsx +++ b/web/oss/src/components/AgentChatSlice/components/ApprovalDock.tsx @@ -1,9 +1,9 @@ import {memo, useEffect, useMemo, useRef, useState} from "react" import {HeightCollapse} from "@agenta/ui" -import {ArrowSquareOut, CaretRight, ChatText, ShieldCheck} from "@phosphor-icons/react" +import {ArrowSquareOut, CaretDown, CaretRight, ChatText, ShieldCheck} from "@phosphor-icons/react" import type {ToolUIPart, UIMessage} from "ai" -import {type GetRef, Button, Input, Switch, Typography} from "antd" +import {type GetRef, Button, Dropdown, Input, Switch, Typography} from "antd" import {useAtomValue} from "jotai" import {useAlwaysAllowTool} from "@/oss/hooks/useAlwaysAllowTool" @@ -59,6 +59,13 @@ const formatInput = (input: unknown): string => { } } +/** One-line, whitespace-collapsed payload preview for the batch peek — so "Approve all" can be + * an informed click without expanding every gate. Truncated; the full payload stays in the card. */ +const inputPreview = (input: unknown): string => { + const s = formatInput(input).replace(/\s+/g, " ").trim() + return s.length > 140 ? `${s.slice(0, 140)}…` : s +} + /** Collapsible exact-payload viewer — the generic approval body, and the fallback (plus the * Build-mode raw view) for specialized renderers. Owns its expand state so a specialized body * can render it anywhere; hosts key it by approval id to recollapse when the gate changes. */ @@ -136,7 +143,7 @@ const ApprovalDock = ({ // pending we FREEZE the shown set so the card holds steady and the dock closes in one step (or, // if only some gates were covered, then steps to the uncovered remainder). const [resolvingIds, setResolvingIds] = useState(null) - const [resolveSource, setResolveSource] = useState<"all" | "one" | null>(null) + const [resolveSource, setResolveSource] = useState<"all" | "deny-all" | "one" | null>(null) const resolving = resolvingIds !== null && approvals.some((a) => resolvingIds.includes(a.approvalId)) // Latch the last non-empty set so the card stays visible while the dock animates closed (a leave @@ -253,6 +260,16 @@ const ApprovalDock = ({ setResolvingIds(shown.map((a) => a.approvalId)) shown.forEach((a) => onApprovalResponse({id: a.approvalId, approved: true})) } + // The explicit turn-level reject — the deny counterpart to "Approve all", never inferred from a + // per-card Deny. Answers every open gate as a warm reject via resume (no teardown); the card + // freezes and the dock closes in one step. No always-allow grant is ever applied on a deny. + const denyAll = () => { + if (responding) return + setResponding(true) + setResolveSource("deny-all") + setResolvingIds(shown.map((a) => a.approvalId)) + shown.forEach((a) => onApprovalResponse({id: a.approvalId, approved: false})) + } // Always mounted; enter + leave animate via the shared HeightCollapse (CSS height + fade, // reduced-motion-proof) — the same primitive the queue, connect banner, and config sections use. @@ -367,13 +384,69 @@ const ApprovalDock = ({ ) : null}
{count > 1 ? ( - + ) : null} {steerEnabled ? (