From 0e1e1b2851e8871721134e86062eee95026e9513 Mon Sep 17 00:00:00 2001 From: Bil0000 <62337003+Bil0000@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:29:54 +0200 Subject: [PATCH 1/6] fix(web): simplify agent approval prompts --- apps/web/src/components/chat/ChatComposer.tsx | 20 ++++-- .../ComposerPendingApprovalActions.test.tsx | 45 ++++---------- .../chat/ComposerPendingApprovalActions.tsx | 62 ++++++++++++++----- .../ComposerPendingApprovalPanel.test.tsx | 18 +----- .../chat/ComposerPendingApprovalPanel.tsx | 31 +++++----- 5 files changed, 89 insertions(+), 87 deletions(-) diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index f7d3ac540ccc..fd2f9996eb74 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -930,6 +930,7 @@ import { PaperclipIcon, PencilRulerIcon, PlayIcon, + ShieldIcon, XIcon, } from "lucide-react"; import { proposedPlanTitle } from "../../proposedPlan"; @@ -6003,20 +6004,27 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) {activePendingApproval ? ( - - + + + + - + { - it("states that the persistent approval lasts for this session", () => { + it("keeps the main decisions visible and secondary decisions in the menu", () => { const markup = renderToStaticMarkup( { />, ); - expect(markup).toContain(">Cancel<"); - expect(markup).toContain("Always allow this session"); - expect(markup).not.toContain(">Always allow<"); - expect(markup).toContain("h-5"); - expect(markup).toContain("sm:text-[11px]"); - expect(markup).not.toContain("sm:h-6"); + expect(markup).toContain(">Decline<"); + expect(markup).toContain(">Approve<"); + expect(markup).not.toContain(">Cancel<"); + expect(markup).not.toContain("Always allow this session"); }); it("shows only the approval choices advertised by an MCP server", () => { @@ -36,48 +34,27 @@ describe("ComposerPendingApprovalActions", () => { />, ); - expect(markup).toContain("Always allow Safari"); + expect(markup).not.toContain("Always allow Safari"); expect(markup).toContain(">Approve<"); expect(markup).not.toContain("Always allow this session"); }); - it("marks an option that carries a provider warning", () => { + it("preserves provider labels for the main decisions", () => { const markup = renderToStaticMarkup( undefined} />, ); - expect(markup).toContain( - 'aria-description="Untrusted files could re-run this action without asking."', - ); - expect(markup).toContain("text-warning"); - expect(markup).toContain("Allow for this thread"); - }); - - it("limits provider-supplied approval labels so narrow rows can wrap", () => { - const label = "Allow ".repeat(40).trim(); - const markup = renderToStaticMarkup( - undefined} - />, - ); - - expect(markup).toContain('class="max-w-40 truncate"'); - expect(markup).toContain(label); + expect(markup).toContain("Allow once"); + expect(markup).toContain("Deny"); + expect(markup).not.toContain(">Approve<"); + expect(markup).not.toContain(">Decline<"); }); }); diff --git a/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx b/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx index 33f5afe50d75..374c94d664b9 100644 --- a/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx +++ b/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx @@ -4,9 +4,11 @@ import { type ProviderApprovalOption, } from "@t3tools/contracts"; import { memo } from "react"; -import { TriangleAlertIcon } from "lucide-react"; +import { ChevronDownIcon, TriangleAlertIcon } from "lucide-react"; import { Button } from "../ui/button"; +import { Menu, MenuItem, MenuPopup, MenuTrigger } from "../ui/menu"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; +import { composerFloatingLayerProps } from "./composerEventScope"; interface ComposerPendingApprovalActionsProps { requestId: ApprovalRequestId; @@ -18,7 +20,6 @@ interface ComposerPendingApprovalActionsProps { ) => Promise; } -const APPROVAL_ACTION_CLASS_NAME = "font-normal"; const DEFAULT_APPROVAL_OPTIONS = [ { decision: "cancel", label: "Cancel" }, { decision: "decline", label: "Decline" }, @@ -32,23 +33,21 @@ export const ComposerPendingApprovalActions = memo(function ComposerPendingAppro options = DEFAULT_APPROVAL_OPTIONS, onRespondToApproval, }: ComposerPendingApprovalActionsProps) { + const primaryOptions = options.filter( + (option) => option.decision === "decline" || option.decision === "accept", + ); + const moreOptions = options.filter( + (option) => option.decision !== "decline" && option.decision !== "accept", + ); + return ( <> - {options.map((option) => { + {primaryOptions.map((option) => { const button = ( ); - // A provider caution, such as a prompt injection warning on "allow - // always", rides along as a tooltip so the row stays one line. return option.warning ? ( @@ -70,6 +67,39 @@ export const ComposerPendingApprovalActions = memo(function ComposerPendingAppro button ); })} + {moreOptions.length > 0 ? ( + + } + > + + + + {moreOptions.map((option) => ( + void onRespondToApproval(requestId, option.decision)} + className="items-start" + > + {option.warning ? : null} + + {option.label} + {option.warning ? ( + {option.warning} + ) : null} + + + ))} + + + ) : null} ); }); diff --git a/apps/web/src/components/chat/ComposerPendingApprovalPanel.test.tsx b/apps/web/src/components/chat/ComposerPendingApprovalPanel.test.tsx index a7ef6bc7d5cb..7a32fdfe95d6 100644 --- a/apps/web/src/components/chat/ComposerPendingApprovalPanel.test.tsx +++ b/apps/web/src/components/chat/ComposerPendingApprovalPanel.test.tsx @@ -19,19 +19,7 @@ describe("ComposerPendingApprovalPanel", () => { />, ); - expect(markup).toContain('data-approval-detail="complete"'); - expect(markup).toContain('aria-label="Command"'); - expect(markup).toContain('role="group"'); - expect(markup).toContain('tabindex="0"'); expect(markup).toContain(detail); - expect(markup).toContain("max-h-20"); - expect(markup).toContain("overflow-auto"); - expect(markup).toContain("whitespace-pre"); - expect(markup).toContain("[scrollbar-width:thin]"); - expect(markup).toContain("[&::-webkit-scrollbar]:h-1.5"); - expect(markup).not.toContain("truncate"); - expect(markup).not.toContain("line-clamp"); - expect(markup).toContain("min-w-0"); expect(markup).not.toContain("Command approval requested"); }); @@ -65,13 +53,11 @@ describe("ComposerPendingApprovalPanel", () => { />, ); - expect(markup).toContain('aria-label="App access approval"'); - expect(markup).toContain('aria-label="App access request"'); expect(markup).toContain(">Safari<"); expect(markup).toContain("Allow ChatGPT to use Safari?"); }); - it("limits long app names so the complete approval message stays readable", () => { + it("preserves the full app name and approval message", () => { const appName = "A".repeat(200); const detail = "Allow ChatGPT to access the selected application?"; const markup = renderToStaticMarkup( @@ -87,9 +73,7 @@ describe("ComposerPendingApprovalPanel", () => { />, ); - expect(markup).toContain("max-w-32 shrink truncate"); expect(markup).toContain(appName); - expect(markup).toContain('data-approval-detail="complete"'); expect(markup).toContain(detail); }); }); diff --git a/apps/web/src/components/chat/ComposerPendingApprovalPanel.tsx b/apps/web/src/components/chat/ComposerPendingApprovalPanel.tsx index deb5ba54c53c..8065556cebc5 100644 --- a/apps/web/src/components/chat/ComposerPendingApprovalPanel.tsx +++ b/apps/web/src/components/chat/ComposerPendingApprovalPanel.tsx @@ -13,6 +13,7 @@ export const ComposerPendingApprovalPanel = memo(function ComposerPendingApprova pendingCount, className, }: ComposerPendingApprovalPanelProps) { + const Detail = approval.requestKind === "mcp-elicitation" ? "span" : "code"; const fallbackLabel = approval.requestKind === "mcp-elicitation" ? "App access approval" @@ -33,27 +34,29 @@ export const ComposerPendingApprovalPanel = memo(function ComposerPendingApprova return ( - {approval.appName ? ( - - {approval.appName} - - ) : null} - + {fallbackLabel} + {approval.appName ? {approval.appName} : null} + {pendingCount > 1 ? ( + 1/{pendingCount} + ) : null} + + {approval.detail || fallbackLabel} - - {pendingCount > 1 ? ( - - 1/{pendingCount} - - ) : null} + ); }); From e3de60d7635c2f7140c95f3ed9be4c5550ed3610 Mon Sep 17 00:00:00 2001 From: Bil0000 <62337003+Bil0000@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:47:17 +0200 Subject: [PATCH 2/6] fix(web): keep approval layout in shared banner variants --- apps/web/src/components/chat/ChatComposer.tsx | 14 +++++--------- apps/web/src/components/chat/ComposerBanner.tsx | 9 +++++++-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index fd2f9996eb74..6294d721ef51 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -6004,27 +6004,23 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) {activePendingApproval ? ( - + - + - + & { - density?: "default" | "comfortable"; + density?: "default" | "comfortable" | "spacious"; placement?: "attached" | "floating"; variant?: ComposerBannerVariant; width?: "fill" | "content"; @@ -163,6 +163,7 @@ function Root({ className={cn( "min-w-0 px-1 pt-(--composer-banner-padding-block) pb-[calc(var(--chat-composer-attachment-overlap)+var(--composer-banner-padding-block))] text-xs/4 [--composer-banner-icon-column:--spacing(7)] [--composer-banner-padding-block:--spacing(1)] sm:[--composer-banner-icon-column:--spacing(6)]", density === "comfortable" && "[--composer-banner-padding-block:--spacing(1.25)]", + density === "spacious" && "px-3 [--composer-banner-padding-block:--spacing(3)]", width === "content" ? "w-fit max-w-full flex-none" : "@container", className, )} @@ -182,7 +183,7 @@ function Row({ layout = "inline", ...props }: useRender.ComponentProps<"div"> & { - layout?: "inline" | "wrap-actions" | "wrap-actions-narrow"; + layout?: "inline" | "wrap-actions" | "wrap-actions-narrow" | "approval"; }) { const rowProps = { className: cn( @@ -193,6 +194,7 @@ function Row({ "@max-[400px]:*:data-[slot=composer-banner-content]:min-h-(--composer-banner-icon-column)", layout === "wrap-actions-narrow" && "@max-[320px]:*:data-[slot=composer-banner-content]:min-h-(--composer-banner-icon-column)", + layout === "approval" && "items-start gap-x-2 gap-y-3", className, ), "data-composer-banner-row": "true", @@ -212,6 +214,7 @@ function Icon({ className, ...props }: ComponentProps<"span">) { data-slot="composer-banner-icon" className={cn( "col-start-1 row-start-1 flex w-(--composer-banner-icon-column) min-w-0 flex-none items-center justify-center text-muted-foreground [&>svg]:size-3", + "group-data-[composer-banner-layout=approval]/banner-row:pt-0.5 group-data-[composer-banner-layout=approval]/banner-row:text-warning group-data-[composer-banner-layout=approval]/banner-row:[&>svg]:size-4", className, )} {...props} @@ -225,6 +228,7 @@ function Content({ className, ...props }: ComponentProps<"span">) { data-slot="composer-banner-content" className={cn( "col-start-2 row-start-1 flex min-w-0 items-center gap-1 *:data-[slot=composer-banner-separator]:mx-0", + "@max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:col-end-4", "group-not-has-[>[data-slot=composer-banner-icon]]/banner-row:col-[1/3] group-not-has-[>[data-slot=composer-banner-icon]]/banner-row:ps-2 sm:group-not-has-[>[data-slot=composer-banner-icon]]/banner-row:ps-1.5", "group-not-has-[>[data-slot=composer-banner-icon],>[data-slot=composer-banner-actions]]/banner-row:pe-2 sm:group-not-has-[>[data-slot=composer-banner-icon],>[data-slot=composer-banner-actions]]/banner-row:pe-1.5", className, @@ -252,6 +256,7 @@ function Actions({ className, ...props }: ComponentProps<"span">) { data-slot="composer-banner-actions" className={cn( "col-start-3 row-start-1 flex flex-wrap items-center justify-end gap-1", + "group-data-[composer-banner-layout=approval]/banner-row:gap-1.5 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:col-start-2 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:col-end-4 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:row-start-2 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:justify-start", "@max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:col-start-2 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:col-end-4 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:row-start-2 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:-ms-2 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:justify-start", "@max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:col-start-2 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:col-end-4 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:row-start-2 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:-ms-2 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:justify-start", className, From 7561069236fa5b1addd41b78a0b0eb5cd9306002 Mon Sep 17 00:00:00 2001 From: Bil0000 <62337003+Bil0000@users.noreply.github.com> Date: Wed, 16 Sep 2026 15:12:11 +0200 Subject: [PATCH 3/6] fix(web): keep every approval choice visible --- .../src/components/chat/ComposerBanner.tsx | 4 +- .../ComposerPendingApprovalActions.test.tsx | 8 +-- .../chat/ComposerPendingApprovalActions.tsx | 59 ++++--------------- 3 files changed, 18 insertions(+), 53 deletions(-) diff --git a/apps/web/src/components/chat/ComposerBanner.tsx b/apps/web/src/components/chat/ComposerBanner.tsx index 423fde4b20ba..6ef346566131 100644 --- a/apps/web/src/components/chat/ComposerBanner.tsx +++ b/apps/web/src/components/chat/ComposerBanner.tsx @@ -228,7 +228,7 @@ function Content({ className, ...props }: ComponentProps<"span">) { data-slot="composer-banner-content" className={cn( "col-start-2 row-start-1 flex min-w-0 items-center gap-1 *:data-[slot=composer-banner-separator]:mx-0", - "@max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:col-end-4", + "group-data-[composer-banner-layout=approval]/banner-row:col-end-4", "group-not-has-[>[data-slot=composer-banner-icon]]/banner-row:col-[1/3] group-not-has-[>[data-slot=composer-banner-icon]]/banner-row:ps-2 sm:group-not-has-[>[data-slot=composer-banner-icon]]/banner-row:ps-1.5", "group-not-has-[>[data-slot=composer-banner-icon],>[data-slot=composer-banner-actions]]/banner-row:pe-2 sm:group-not-has-[>[data-slot=composer-banner-icon],>[data-slot=composer-banner-actions]]/banner-row:pe-1.5", className, @@ -256,7 +256,7 @@ function Actions({ className, ...props }: ComponentProps<"span">) { data-slot="composer-banner-actions" className={cn( "col-start-3 row-start-1 flex flex-wrap items-center justify-end gap-1", - "group-data-[composer-banner-layout=approval]/banner-row:gap-1.5 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:col-start-2 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:col-end-4 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:row-start-2 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:justify-start", + "group-data-[composer-banner-layout=approval]/banner-row:gap-1.5 group-data-[composer-banner-layout=approval]/banner-row:col-start-2 group-data-[composer-banner-layout=approval]/banner-row:col-end-4 group-data-[composer-banner-layout=approval]/banner-row:row-start-2 group-data-[composer-banner-layout=approval]/banner-row:justify-start", "@max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:col-start-2 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:col-end-4 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:row-start-2 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:-ms-2 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:justify-start", "@max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:col-start-2 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:col-end-4 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:row-start-2 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:-ms-2 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:justify-start", className, diff --git a/apps/web/src/components/chat/ComposerPendingApprovalActions.test.tsx b/apps/web/src/components/chat/ComposerPendingApprovalActions.test.tsx index 2fb87376eac0..9360c5a1baf1 100644 --- a/apps/web/src/components/chat/ComposerPendingApprovalActions.test.tsx +++ b/apps/web/src/components/chat/ComposerPendingApprovalActions.test.tsx @@ -5,7 +5,7 @@ import { describe, expect, it } from "vite-plus/test"; import { ComposerPendingApprovalActions } from "./ComposerPendingApprovalActions"; describe("ComposerPendingApprovalActions", () => { - it("keeps the main decisions visible and secondary decisions in the menu", () => { + it("shows every default approval choice without opening a menu", () => { const markup = renderToStaticMarkup( { expect(markup).toContain(">Decline<"); expect(markup).toContain(">Approve<"); - expect(markup).not.toContain(">Cancel<"); - expect(markup).not.toContain("Always allow this session"); + expect(markup).toContain(">Cancel<"); + expect(markup).toContain("Always allow this session"); }); it("shows only the approval choices advertised by an MCP server", () => { @@ -34,7 +34,7 @@ describe("ComposerPendingApprovalActions", () => { />, ); - expect(markup).not.toContain("Always allow Safari"); + expect(markup).toContain("Always allow Safari"); expect(markup).toContain(">Approve<"); expect(markup).not.toContain("Always allow this session"); }); diff --git a/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx b/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx index 374c94d664b9..7820f42b2e02 100644 --- a/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx +++ b/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx @@ -4,11 +4,9 @@ import { type ProviderApprovalOption, } from "@t3tools/contracts"; import { memo } from "react"; -import { ChevronDownIcon, TriangleAlertIcon } from "lucide-react"; +import { TriangleAlertIcon } from "lucide-react"; import { Button } from "../ui/button"; -import { Menu, MenuItem, MenuPopup, MenuTrigger } from "../ui/menu"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; -import { composerFloatingLayerProps } from "./composerEventScope"; interface ComposerPendingApprovalActionsProps { requestId: ApprovalRequestId; @@ -33,27 +31,27 @@ export const ComposerPendingApprovalActions = memo(function ComposerPendingAppro options = DEFAULT_APPROVAL_OPTIONS, onRespondToApproval, }: ComposerPendingApprovalActionsProps) { - const primaryOptions = options.filter( - (option) => option.decision === "decline" || option.decision === "accept", - ); - const moreOptions = options.filter( - (option) => option.decision !== "decline" && option.decision !== "accept", - ); - return ( <> - {primaryOptions.map((option) => { + {options.map((option) => { const button = ( ); return option.warning ? ( @@ -67,39 +65,6 @@ export const ComposerPendingApprovalActions = memo(function ComposerPendingAppro button ); })} - {moreOptions.length > 0 ? ( - - } - > - - - - {moreOptions.map((option) => ( - void onRespondToApproval(requestId, option.decision)} - className="items-start" - > - {option.warning ? : null} - - {option.label} - {option.warning ? ( - {option.warning} - ) : null} - - - ))} - - - ) : null} ); }); From e17af408c65f86a58b7239c27e576f92f5a0f351 Mon Sep 17 00:00:00 2001 From: Bil0000 <62337003+Bil0000@users.noreply.github.com> Date: Wed, 16 Sep 2026 15:17:16 +0200 Subject: [PATCH 4/6] fix(web): center approval actions beside a right-arrow menu --- .../src/components/chat/ComposerBanner.tsx | 4 +- .../ComposerPendingApprovalActions.test.tsx | 10 ++-- .../chat/ComposerPendingApprovalActions.tsx | 59 +++++++++++++++---- 3 files changed, 54 insertions(+), 19 deletions(-) diff --git a/apps/web/src/components/chat/ComposerBanner.tsx b/apps/web/src/components/chat/ComposerBanner.tsx index 6ef346566131..d81d6c1aa506 100644 --- a/apps/web/src/components/chat/ComposerBanner.tsx +++ b/apps/web/src/components/chat/ComposerBanner.tsx @@ -228,7 +228,7 @@ function Content({ className, ...props }: ComponentProps<"span">) { data-slot="composer-banner-content" className={cn( "col-start-2 row-start-1 flex min-w-0 items-center gap-1 *:data-[slot=composer-banner-separator]:mx-0", - "group-data-[composer-banner-layout=approval]/banner-row:col-end-4", + "@max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:col-end-4", "group-not-has-[>[data-slot=composer-banner-icon]]/banner-row:col-[1/3] group-not-has-[>[data-slot=composer-banner-icon]]/banner-row:ps-2 sm:group-not-has-[>[data-slot=composer-banner-icon]]/banner-row:ps-1.5", "group-not-has-[>[data-slot=composer-banner-icon],>[data-slot=composer-banner-actions]]/banner-row:pe-2 sm:group-not-has-[>[data-slot=composer-banner-icon],>[data-slot=composer-banner-actions]]/banner-row:pe-1.5", className, @@ -256,7 +256,7 @@ function Actions({ className, ...props }: ComponentProps<"span">) { data-slot="composer-banner-actions" className={cn( "col-start-3 row-start-1 flex flex-wrap items-center justify-end gap-1", - "group-data-[composer-banner-layout=approval]/banner-row:gap-1.5 group-data-[composer-banner-layout=approval]/banner-row:col-start-2 group-data-[composer-banner-layout=approval]/banner-row:col-end-4 group-data-[composer-banner-layout=approval]/banner-row:row-start-2 group-data-[composer-banner-layout=approval]/banner-row:justify-start", + "group-data-[composer-banner-layout=approval]/banner-row:self-center group-data-[composer-banner-layout=approval]/banner-row:gap-1.5 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:col-start-2 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:col-end-4 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:row-start-2", "@max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:col-start-2 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:col-end-4 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:row-start-2 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:-ms-2 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:justify-start", "@max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:col-start-2 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:col-end-4 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:row-start-2 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:-ms-2 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:justify-start", className, diff --git a/apps/web/src/components/chat/ComposerPendingApprovalActions.test.tsx b/apps/web/src/components/chat/ComposerPendingApprovalActions.test.tsx index 9360c5a1baf1..0db4b9f76aaf 100644 --- a/apps/web/src/components/chat/ComposerPendingApprovalActions.test.tsx +++ b/apps/web/src/components/chat/ComposerPendingApprovalActions.test.tsx @@ -5,7 +5,7 @@ import { describe, expect, it } from "vite-plus/test"; import { ComposerPendingApprovalActions } from "./ComposerPendingApprovalActions"; describe("ComposerPendingApprovalActions", () => { - it("shows every default approval choice without opening a menu", () => { + it("keeps the main decisions visible and secondary decisions in the menu", () => { const markup = renderToStaticMarkup( { expect(markup).toContain(">Decline<"); expect(markup).toContain(">Approve<"); - expect(markup).toContain(">Cancel<"); - expect(markup).toContain("Always allow this session"); + expect(markup).not.toContain(">Cancel<"); + expect(markup).not.toContain("Always allow this session"); }); - it("shows only the approval choices advertised by an MCP server", () => { + it("keeps secondary provider labels out of the compact action row", () => { const markup = renderToStaticMarkup( { />, ); - expect(markup).toContain("Always allow Safari"); + expect(markup).not.toContain("Always allow Safari"); expect(markup).toContain(">Approve<"); expect(markup).not.toContain("Always allow this session"); }); diff --git a/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx b/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx index 7820f42b2e02..82beb7e9b1a0 100644 --- a/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx +++ b/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx @@ -4,9 +4,11 @@ import { type ProviderApprovalOption, } from "@t3tools/contracts"; import { memo } from "react"; -import { TriangleAlertIcon } from "lucide-react"; +import { ChevronRightIcon, TriangleAlertIcon } from "lucide-react"; import { Button } from "../ui/button"; +import { Menu, MenuItem, MenuPopup, MenuTrigger } from "../ui/menu"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; +import { composerFloatingLayerProps } from "./composerEventScope"; interface ComposerPendingApprovalActionsProps { requestId: ApprovalRequestId; @@ -31,27 +33,27 @@ export const ComposerPendingApprovalActions = memo(function ComposerPendingAppro options = DEFAULT_APPROVAL_OPTIONS, onRespondToApproval, }: ComposerPendingApprovalActionsProps) { + const primaryOptions = options.filter( + (option) => option.decision === "decline" || option.decision === "accept", + ); + const moreOptions = options.filter( + (option) => option.decision !== "decline" && option.decision !== "accept", + ); + return ( <> - {options.map((option) => { + {primaryOptions.map((option) => { const button = ( ); return option.warning ? ( @@ -65,6 +67,39 @@ export const ComposerPendingApprovalActions = memo(function ComposerPendingAppro button ); })} + {moreOptions.length > 0 ? ( + + } + > + + + + {moreOptions.map((option) => ( + void onRespondToApproval(requestId, option.decision)} + className="items-start" + > + {option.warning ? : null} + + {option.label} + {option.warning ? ( + {option.warning} + ) : null} + + + ))} + + + ) : null} ); }); From b6a2fe4a72fcbfd706fc68afb68478c6844d49aa Mon Sep 17 00:00:00 2001 From: Bil0000 <62337003+Bil0000@users.noreply.github.com> Date: Wed, 16 Sep 2026 16:12:26 +0200 Subject: [PATCH 5/6] fix(web): compact secondary approval buttons and warnings --- .../chat/ComposerPendingApprovalActions.tsx | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx b/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx index 82beb7e9b1a0..60936e330fc0 100644 --- a/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx +++ b/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx @@ -5,7 +5,7 @@ import { } from "@t3tools/contracts"; import { memo } from "react"; import { ChevronRightIcon, TriangleAlertIcon } from "lucide-react"; -import { Button } from "../ui/button"; +import { Button, buttonVariants } from "../ui/button"; import { Menu, MenuItem, MenuPopup, MenuTrigger } from "../ui/menu"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { composerFloatingLayerProps } from "./composerEventScope"; @@ -79,24 +79,36 @@ export const ComposerPendingApprovalActions = memo(function ComposerPendingAppro {...composerFloatingLayerProps} side="top" align="end" - className="max-w-[min(22rem,calc(100vw-2rem))]" + className="w-56 max-w-[calc(100vw-2rem)]" > - {moreOptions.map((option) => ( - void onRespondToApproval(requestId, option.decision)} - className="items-start" - > - {option.warning ? : null} - - {option.label} - {option.warning ? ( - {option.warning} - ) : null} - - - ))} + {moreOptions.map((option) => { + const item = ( + void onRespondToApproval(requestId, option.decision)} + className={buttonVariants({ + variant: "outline", + size: "compact", + className: "mb-1 h-auto min-h-7 w-full last:mb-0 sm:text-xs", + })} + > + {option.warning ? : null} + {option.label} + + ); + return option.warning ? ( + + + + {option.warning} + + + ) : ( + item + ); + })} ) : null} From b6a7297a7a8aa657d5d547831fe46c8684a0f7f6 Mon Sep 17 00:00:00 2001 From: Bil0000 <62337003+Bil0000@users.noreply.github.com> Date: Wed, 16 Sep 2026 16:21:56 +0200 Subject: [PATCH 6/6] fix(web): use three-dot approval menu with ghost actions --- .../chat/ComposerPendingApprovalActions.tsx | 13 +++++-------- apps/web/src/components/ui/menu.tsx | 9 ++++++++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx b/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx index 60936e330fc0..743d846dc5e2 100644 --- a/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx +++ b/apps/web/src/components/chat/ComposerPendingApprovalActions.tsx @@ -4,8 +4,8 @@ import { type ProviderApprovalOption, } from "@t3tools/contracts"; import { memo } from "react"; -import { ChevronRightIcon, TriangleAlertIcon } from "lucide-react"; -import { Button, buttonVariants } from "../ui/button"; +import { EllipsisIcon, TriangleAlertIcon } from "lucide-react"; +import { Button } from "../ui/button"; import { Menu, MenuItem, MenuPopup, MenuTrigger } from "../ui/menu"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { composerFloatingLayerProps } from "./composerEventScope"; @@ -73,7 +73,7 @@ export const ComposerPendingApprovalActions = memo(function ComposerPendingAppro disabled={isResponding} render={