From 35f7b28373b40251ff9b7c8de6a43202ccc844eb Mon Sep 17 00:00:00 2001 From: Arda Erzin Date: Thu, 23 Jul 2026 00:18:45 +0200 Subject: [PATCH] =?UTF-8?q?feat(agent-chat):=20batch=20approvals=20?= =?UTF-8?q?=E2=80=94=20Deny=20all=20+=20context=20peek?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a turn parks on 2+ approval gates the dock still shows one card at a time, so "Approve all" was a blind click over the hidden siblings. Turn the batch "Approve all" into a split button: the primary click still approves the whole batch, but the caret opens a peek that lists every pending action (friendly tool name + one-line payload preview) so approving all is informed, and offers the explicit turn-level "Deny all". Deny all is the deny counterpart to Approve all — a warm reject of every open gate via resume (no teardown), never inferred from a per-card Deny. Per-card Deny/Approve are unchanged for stepping one at a time. FE-only; a per-row read/write risk chip is a later increment once the runner supplies the hint. --- .../components/ApprovalDock.tsx | 81 +++++++++++++++++-- 1 file changed, 75 insertions(+), 6 deletions(-) diff --git a/web/oss/src/components/AgentChatSlice/components/ApprovalDock.tsx b/web/oss/src/components/AgentChatSlice/components/ApprovalDock.tsx index 2f2ba36e0c..50e954dbd7 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, ShieldCheck} from "@phosphor-icons/react" +import {ArrowSquareOut, CaretDown, CaretRight, ShieldCheck} from "@phosphor-icons/react" import type {ToolUIPart, UIMessage} from "ai" -import {Button, Switch, Typography} from "antd" +import {Button, Dropdown, Switch, Typography} from "antd" import {useAtomValue} from "jotai" import {useAlwaysAllowTool} from "@/oss/hooks/useAlwaysAllowTool" @@ -58,6 +58,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. */ @@ -135,7 +142,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 @@ -220,6 +227,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. @@ -329,13 +346,65 @@ const ApprovalDock = ({ ) : null}
{count > 1 ? ( - + ) : null}