| author | |
| committer | |
| log | 01e64193d9d2abfae8f7b3d20f66824e38397bd6 |
| tree | f2b24958ae1c6afcb655e22b4c2563b88624a5ba |
| parent | f1b497a5552abf4616a7477b730b78a1274f3db4 |
| signature | Signed by PGP key B5690EEEBB952194 |
7 files changed, 108 insertions(+), 89 deletions(-)
apps/web/src/components/chat/ChatComposer.tsx+8-4| ... | ... | @@ -933,6 +933,7 @@ import { |
| 933 | 933 | PaperclipIcon, |
| 934 | 934 | PencilRulerIcon, |
| 935 | 935 | PlayIcon, |
| 936 | ShieldIcon, | |
| 936 | 937 | XIcon, |
| 937 | 938 | } from "lucide-react"; |
| 938 | 939 | import { proposedPlanTitle } from "../../proposedPlan"; |
| ... | ... | @@ -6042,13 +6043,16 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 6042 | 6043 | <ComposerBanner.Root |
| 6043 | 6044 | data-chat-composer-top-drawer="true" |
| 6044 | 6045 | variant={activePendingApproval ? "warning" : "info"} |
| 6046 | density={activePendingApproval ? "spacious" : "default"} | |
| 6045 | 6047 | > |
| 6046 | 6048 | {activePendingApproval ? ( |
| 6047 | 6049 | <ComposerBanner.Row |
| 6048 | layout="wrap-actions" | |
| 6050 | layout="approval" | |
| 6049 | 6051 | data-chat-composer-collapsed-controls="true" |
| 6050 | 6052 | > |
| 6051 | <ComposerBanner.Icon /> | |
| 6053 | <ComposerBanner.Icon> | |
| 6054 | <ShieldIcon /> | |
| 6055 | </ComposerBanner.Icon> | |
| 6052 | 6056 | <ComposerBanner.Content> |
| 6053 | 6057 | <ComposerPendingApprovalPanel |
| 6054 | 6058 | approval={activePendingApproval} |
| ... | ... | @@ -6712,6 +6716,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 6712 | 6716 | showMobilePendingAnswerActions && "max-sm:pb-11", |
| 6713 | 6717 | isComposerResting && |
| 6714 | 6718 | "max-h-8 min-h-8 overflow-hidden whitespace-pre! leading-8", |
| 6719 | isComposerApprovalState && "min-h-8", | |
| 6715 | 6720 | )} |
| 6716 | 6721 | placeholderClassName={cn( |
| 6717 | 6722 | isComposerResting && |
| ... | ... | @@ -6727,8 +6732,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 6727 | 6732 | onPaste={onComposerPaste} |
| 6728 | 6733 | placeholder={ |
| 6729 | 6734 | isComposerApprovalState |
| 6730 | ? (activePendingApproval?.detail ?? | |
| 6731 | "Resolve this approval request to continue") | |
| 6735 | ? "Resolve this approval request to continue" | |
| 6732 | 6736 | : activePendingProgress |
| 6733 | 6737 | ? isChoiceOnlyPendingQuestion |
| 6734 | 6738 | ? "Choose an option above" |
apps/web/src/components/chat/ComposerBanner.tsx+7-2| ... | ... | @@ -153,7 +153,7 @@ function Root({ |
| 153 | 153 | width = "fill", |
| 154 | 154 | ...props |
| 155 | 155 | }: ComponentProps<"div"> & { |
| 156 | density?: "default" | "comfortable"; | |
| 156 | density?: "default" | "comfortable" | "spacious"; | |
| 157 | 157 | placement?: "attached" | "floating"; |
| 158 | 158 | variant?: ComposerBannerVariant; |
| 159 | 159 | width?: "fill" | "content"; |
| ... | ... | @@ -163,6 +163,7 @@ function Root({ |
| 163 | 163 | className={cn( |
| 164 | 164 | "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)]", |
| 165 | 165 | density === "comfortable" && "[--composer-banner-padding-block:--spacing(1.25)]", |
| 166 | density === "spacious" && "px-3 [--composer-banner-padding-block:--spacing(3)]", | |
| 166 | 167 | width === "content" ? "w-fit max-w-full flex-none" : "@container", |
| 167 | 168 | className, |
| 168 | 169 | )} |
| ... | ... | @@ -182,7 +183,7 @@ function Row({ |
| 182 | 183 | layout = "inline", |
| 183 | 184 | ...props |
| 184 | 185 | }: useRender.ComponentProps<"div"> & { |
| 185 | layout?: "inline" | "wrap-actions" | "wrap-actions-narrow"; | |
| 186 | layout?: "inline" | "wrap-actions" | "wrap-actions-narrow" | "approval"; | |
| 186 | 187 | }) { |
| 187 | 188 | const rowProps = { |
| 188 | 189 | className: cn( |
| ... | ... | @@ -193,6 +194,7 @@ function Row({ |
| 193 | 194 | "@max-[400px]:*:data-[slot=composer-banner-content]:min-h-(--composer-banner-icon-column)", |
| 194 | 195 | layout === "wrap-actions-narrow" && |
| 195 | 196 | "@max-[320px]:*:data-[slot=composer-banner-content]:min-h-(--composer-banner-icon-column)", |
| 197 | layout === "approval" && "items-start gap-x-2 gap-y-3", | |
| 196 | 198 | className, |
| 197 | 199 | ), |
| 198 | 200 | "data-composer-banner-row": "true", |
| ... | ... | @@ -212,6 +214,7 @@ function Icon({ className, ...props }: ComponentProps<"span">) { |
| 212 | 214 | data-slot="composer-banner-icon" |
| 213 | 215 | className={cn( |
| 214 | 216 | "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", |
| 217 | "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", | |
| 215 | 218 | className, |
| 216 | 219 | )} |
| 217 | 220 | {...props} |
| ... | ... | @@ -225,6 +228,7 @@ function Content({ className, ...props }: ComponentProps<"span">) { |
| 225 | 228 | data-slot="composer-banner-content" |
| 226 | 229 | className={cn( |
| 227 | 230 | "col-start-2 row-start-1 flex min-w-0 items-center gap-1 *:data-[slot=composer-banner-separator]:mx-0", |
| 231 | "@max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:col-end-4", | |
| 228 | 232 | "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", |
| 229 | 233 | "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", |
| 230 | 234 | className, |
| ... | ... | @@ -252,6 +256,7 @@ function Actions({ className, ...props }: ComponentProps<"span">) { |
| 252 | 256 | data-slot="composer-banner-actions" |
| 253 | 257 | className={cn( |
| 254 | 258 | "col-start-3 row-start-1 flex flex-wrap items-center justify-end gap-1", |
| 259 | "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", | |
| 255 | 260 | "@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", |
| 256 | 261 | "@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", |
| 257 | 262 | className, |
apps/web/src/components/chat/ComposerPendingApprovalActions.test.tsx+12-35| ... | ... | @@ -5,7 +5,7 @@ import { describe, expect, it } from "vite-plus/test"; |
| 5 | 5 | import { ComposerPendingApprovalActions } from "./ComposerPendingApprovalActions"; |
| 6 | 6 | |
| 7 | 7 | describe("ComposerPendingApprovalActions", () => { |
| 8 | it("states that the persistent approval lasts for this session", () => { | |
| 8 | it("keeps the main decisions visible and secondary decisions in the menu", () => { | |
| 9 | 9 | const markup = renderToStaticMarkup( |
| 10 | 10 | <ComposerPendingApprovalActions |
| 11 | 11 | requestId={ApprovalRequestId.make("approval-1")} |
| ... | ... | @@ -14,15 +14,13 @@ describe("ComposerPendingApprovalActions", () => { |
| 14 | 14 | />, |
| 15 | 15 | ); |
| 16 | 16 | |
| 17 | expect(markup).toContain(">Cancel<"); | |
| 18 | expect(markup).toContain("Always allow this session"); | |
| 19 | expect(markup).not.toContain(">Always allow<"); | |
| 20 | expect(markup).toContain("h-5"); | |
| 21 | expect(markup).toContain("sm:text-[11px]"); | |
| 22 | expect(markup).not.toContain("sm:h-6"); | |
| 17 | expect(markup).toContain(">Decline<"); | |
| 18 | expect(markup).toContain(">Approve<"); | |
| 19 | expect(markup).not.toContain(">Cancel<"); | |
| 20 | expect(markup).not.toContain("Always allow this session"); | |
| 23 | 21 | }); |
| 24 | 22 | |
| 25 | it("shows only the approval choices advertised by an MCP server", () => { | |
| 23 | it("keeps secondary provider labels out of the compact action row", () => { | |
| 26 | 24 | const markup = renderToStaticMarkup( |
| 27 | 25 | <ComposerPendingApprovalActions |
| 28 | 26 | requestId={ApprovalRequestId.make("approval-safari")} |
| ... | ... | @@ -36,48 +34,27 @@ describe("ComposerPendingApprovalActions", () => { |
| 36 | 34 | />, |
| 37 | 35 | ); |
| 38 | 36 | |
| 39 | expect(markup).toContain("Always allow Safari"); | |
| 37 | expect(markup).not.toContain("Always allow Safari"); | |
| 40 | 38 | expect(markup).toContain(">Approve<"); |
| 41 | 39 | expect(markup).not.toContain("Always allow this session"); |
| 42 | 40 | }); |
| 43 | 41 | |
| 44 | it("marks an option that carries a provider warning", () => { | |
| 42 | it("preserves provider labels for the main decisions", () => { | |
| 45 | 43 | const markup = renderToStaticMarkup( |
| 46 | 44 | <ComposerPendingApprovalActions |
| 47 | 45 | requestId={ApprovalRequestId.make("approval-1")} |
| 48 | 46 | isResponding={false} |
| 49 | 47 | options={[ |
| 50 | 48 | { decision: "accept", label: "Allow once" }, |
| 51 | { | |
| 52 | decision: "acceptForSession", | |
| 53 | label: "Allow for this thread", | |
| 54 | warning: "Untrusted files could re-run this action without asking.", | |
| 55 | }, | |
| 56 | 49 | { decision: "decline", label: "Deny" }, |
| 57 | 50 | ]} |
| 58 | 51 | onRespondToApproval={async () => undefined} |
| 59 | 52 | />, |
| 60 | 53 | ); |
| 61 | 54 | |
| 62 | expect(markup).toContain( | |
| 63 | 'aria-description="Untrusted files could re-run this action without asking."', | |
| 64 | ); | |
| 65 | expect(markup).toContain("text-warning"); | |
| 66 | expect(markup).toContain("Allow for this thread"); | |
| 67 | }); | |
| 68 | ||
| 69 | it("limits provider-supplied approval labels so narrow rows can wrap", () => { | |
| 70 | const label = "Allow ".repeat(40).trim(); | |
| 71 | const markup = renderToStaticMarkup( | |
| 72 | <ComposerPendingApprovalActions | |
| 73 | requestId={ApprovalRequestId.make("approval-long-label")} | |
| 74 | isResponding={false} | |
| 75 | options={[{ decision: "acceptAlways", label }]} | |
| 76 | onRespondToApproval={async () => undefined} | |
| 77 | />, | |
| 78 | ); | |
| 79 | ||
| 80 | expect(markup).toContain('class="max-w-40 truncate"'); | |
| 81 | expect(markup).toContain(label); | |
| 55 | expect(markup).toContain("Allow once"); | |
| 56 | expect(markup).toContain("Deny"); | |
| 57 | expect(markup).not.toContain(">Approve<"); | |
| 58 | expect(markup).not.toContain(">Decline<"); | |
| 82 | 59 | }); |
| 83 | 60 | }); |
apps/web/src/components/chat/ComposerPendingApprovalActions.tsx+55-16| ... | ... | @@ -4,9 +4,11 @@ import { |
| 4 | 4 | type ProviderApprovalOption, |
| 5 | 5 | } from "@t3tools/contracts"; |
| 6 | 6 | import { memo } from "react"; |
| 7 | import { TriangleAlertIcon } from "lucide-react"; | |
| 7 | import { EllipsisIcon, TriangleAlertIcon } from "lucide-react"; | |
| 8 | 8 | import { Button } from "../ui/button"; |
| 9 | import { Menu, MenuItem, MenuPopup, MenuTrigger } from "../ui/menu"; | |
| 9 | 10 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 11 | import { composerFloatingLayerProps } from "./composerEventScope"; | |
| 10 | 12 | |
| 11 | 13 | interface ComposerPendingApprovalActionsProps { |
| 12 | 14 | requestId: ApprovalRequestId; |
| ... | ... | @@ -18,7 +20,6 @@ interface ComposerPendingApprovalActionsProps { |
| 18 | 20 | ) => Promise<unknown>; |
| 19 | 21 | } |
| 20 | 22 | |
| 21 | const APPROVAL_ACTION_CLASS_NAME = "font-normal"; | |
| 22 | 23 | const DEFAULT_APPROVAL_OPTIONS = [ |
| 23 | 24 | { decision: "cancel", label: "Cancel" }, |
| 24 | 25 | { decision: "decline", label: "Decline" }, |
| ... | ... | @@ -32,23 +33,21 @@ export const ComposerPendingApprovalActions = memo(function ComposerPendingAppro |
| 32 | 33 | options = DEFAULT_APPROVAL_OPTIONS, |
| 33 | 34 | onRespondToApproval, |
| 34 | 35 | }: ComposerPendingApprovalActionsProps) { |
| 36 | const primaryOptions = options.filter( | |
| 37 | (option) => option.decision === "decline" || option.decision === "accept", | |
| 38 | ); | |
| 39 | const moreOptions = options.filter( | |
| 40 | (option) => option.decision !== "decline" && option.decision !== "accept", | |
| 41 | ); | |
| 42 | ||
| 35 | 43 | return ( |
| 36 | 44 | <> |
| 37 | {options.map((option) => { | |
| 45 | {primaryOptions.map((option) => { | |
| 38 | 46 | const button = ( |
| 39 | 47 | <Button |
| 40 | 48 | key={option.decision} |
| 41 | size="micro" | |
| 42 | variant="ghost-muted" | |
| 43 | className={`${APPROVAL_ACTION_CLASS_NAME}${ | |
| 44 | option.decision === "decline" | |
| 45 | ? " text-destructive-foreground [:hover,[data-pressed]]:text-destructive-foreground" | |
| 46 | : option.decision === "accept" | |
| 47 | ? " text-foreground" | |
| 48 | : option.warning | |
| 49 | ? " text-warning" | |
| 50 | : "" | |
| 51 | }`} | |
| 49 | size="xs" | |
| 50 | variant={option.decision === "accept" ? "default" : "outline"} | |
| 52 | 51 | disabled={isResponding} |
| 53 | 52 | aria-description={option.warning} |
| 54 | 53 | onClick={() => void onRespondToApproval(requestId, option.decision)} |
| ... | ... | @@ -57,8 +56,6 @@ export const ComposerPendingApprovalActions = memo(function ComposerPendingAppro |
| 57 | 56 | <span className="max-w-40 truncate">{option.label}</span> |
| 58 | 57 | </Button> |
| 59 | 58 | ); |
| 60 | // A provider caution, such as a prompt injection warning on "allow | |
| 61 | // always", rides along as a tooltip so the row stays one line. | |
| 62 | 59 | return option.warning ? ( |
| 63 | 60 | <Tooltip key={option.decision}> |
| 64 | 61 | <TooltipTrigger render={button} /> |
| ... | ... | @@ -70,6 +67,48 @@ export const ComposerPendingApprovalActions = memo(function ComposerPendingAppro |
| 70 | 67 | button |
| 71 | 68 | ); |
| 72 | 69 | })} |
| 70 | {moreOptions.length > 0 ? ( | |
| 71 | <Menu> | |
| 72 | <MenuTrigger | |
| 73 | disabled={isResponding} | |
| 74 | render={<Button size="icon-xs" variant="outline" aria-label="More approval options" />} | |
| 75 | > | |
| 76 | <EllipsisIcon /> | |
| 77 | </MenuTrigger> | |
| 78 | <MenuPopup | |
| 79 | {...composerFloatingLayerProps} | |
| 80 | side="top" | |
| 81 | align="end" | |
| 82 | className="w-56 max-w-[calc(100vw-2rem)]" | |
| 83 | > | |
| 84 | {moreOptions.map((option) => { | |
| 85 | const item = ( | |
| 86 | <MenuItem | |
| 87 | key={option.decision} | |
| 88 | disabled={isResponding} | |
| 89 | aria-description={option.warning} | |
| 90 | onClick={() => void onRespondToApproval(requestId, option.decision)} | |
| 91 | variant="ghost" | |
| 92 | className="mb-1 last:mb-0" | |
| 93 | > | |
| 94 | {option.warning ? <TriangleAlertIcon className="size-3 text-warning" /> : null} | |
| 95 | <span className="min-w-0 whitespace-normal wrap-break-word">{option.label}</span> | |
| 96 | </MenuItem> | |
| 97 | ); | |
| 98 | return option.warning ? ( | |
| 99 | <Tooltip key={option.decision}> | |
| 100 | <TooltipTrigger render={item} /> | |
| 101 | <TooltipPopup side="top" className="max-w-64 text-xs leading-snug"> | |
| 102 | {option.warning} | |
| 103 | </TooltipPopup> | |
| 104 | </Tooltip> | |
| 105 | ) : ( | |
| 106 | item | |
| 107 | ); | |
| 108 | })} | |
| 109 | </MenuPopup> | |
| 110 | </Menu> | |
| 111 | ) : null} | |
| 73 | 112 | </> |
| 74 | 113 | ); |
| 75 | 114 | }); |
apps/web/src/components/chat/ComposerPendingApprovalPanel.test.tsx+1-17| ... | ... | @@ -19,19 +19,7 @@ describe("ComposerPendingApprovalPanel", () => { |
| 19 | 19 | />, |
| 20 | 20 | ); |
| 21 | 21 | |
| 22 | expect(markup).toContain('data-approval-detail="complete"'); | |
| 23 | expect(markup).toContain('aria-label="Command"'); | |
| 24 | expect(markup).toContain('role="group"'); | |
| 25 | expect(markup).toContain('tabindex="0"'); | |
| 26 | 22 | expect(markup).toContain(detail); |
| 27 | expect(markup).toContain("max-h-20"); | |
| 28 | expect(markup).toContain("overflow-auto"); | |
| 29 | expect(markup).toContain("whitespace-pre"); | |
| 30 | expect(markup).toContain("[scrollbar-width:thin]"); | |
| 31 | expect(markup).toContain("[&amp;::-webkit-scrollbar]:h-1.5"); | |
| 32 | expect(markup).not.toContain("truncate"); | |
| 33 | expect(markup).not.toContain("line-clamp"); | |
| 34 | expect(markup).toContain("min-w-0"); | |
| 35 | 23 | expect(markup).not.toContain("Command approval requested"); |
| 36 | 24 | }); |
| 37 | 25 | |
| ... | ... | @@ -65,13 +53,11 @@ describe("ComposerPendingApprovalPanel", () => { |
| 65 | 53 | />, |
| 66 | 54 | ); |
| 67 | 55 | |
| 68 | expect(markup).toContain('aria-label="App access approval"'); | |
| 69 | expect(markup).toContain('aria-label="App access request"'); | |
| 70 | 56 | expect(markup).toContain(">Safari<"); |
| 71 | 57 | expect(markup).toContain("Allow ChatGPT to use Safari?"); |
| 72 | 58 | }); |
| 73 | 59 | |
| 74 | it("limits long app names so the complete approval message stays readable", () => { | |
| 60 | it("preserves the full app name and approval message", () => { | |
| 75 | 61 | const appName = "A".repeat(200); |
| 76 | 62 | const detail = "Allow ChatGPT to access the selected application?"; |
| 77 | 63 | const markup = renderToStaticMarkup( |
| ... | ... | @@ -87,9 +73,7 @@ describe("ComposerPendingApprovalPanel", () => { |
| 87 | 73 | />, |
| 88 | 74 | ); |
| 89 | 75 | |
| 90 | expect(markup).toContain("max-w-32 shrink truncate"); | |
| 91 | 76 | expect(markup).toContain(appName); |
| 92 | expect(markup).toContain('data-approval-detail="complete"'); | |
| 93 | 77 | expect(markup).toContain(detail); |
| 94 | 78 | }); |
| 95 | 79 | }); |
apps/web/src/components/chat/ComposerPendingApprovalPanel.tsx+17-14| ... | ... | @@ -13,6 +13,7 @@ export const ComposerPendingApprovalPanel = memo(function ComposerPendingApprova |
| 13 | 13 | pendingCount, |
| 14 | 14 | className, |
| 15 | 15 | }: ComposerPendingApprovalPanelProps) { |
| 16 | const Detail = approval.requestKind === "mcp-elicitation" ? "span" : "code"; | |
| 16 | 17 | const fallbackLabel = |
| 17 | 18 | approval.requestKind === "mcp-elicitation" |
| 18 | 19 | ? "App access approval" |
| ... | ... | @@ -33,27 +34,29 @@ export const ComposerPendingApprovalPanel = memo(function ComposerPendingApprova |
| 33 | 34 | return ( |
| 34 | 35 | <span |
| 35 | 36 | aria-label={fallbackLabel} |
| 36 | className={cn("flex min-w-0 flex-1 items-center gap-2", className)} | |
| 37 | className={cn("flex min-w-0 flex-1 flex-col items-start gap-1", className)} | |
| 37 | 38 | role="group" |
| 38 | 39 | > |
| 39 | {approval.appName ? ( | |
| 40 | <span className="max-w-32 shrink truncate text-[11px] font-medium text-foreground"> | |
| 41 | {approval.appName} | |
| 42 | </span> | |
| 43 | ) : null} | |
| 44 | <code | |
| 40 | <span className="flex w-full min-w-0 items-center gap-2 text-[11px] text-muted-foreground"> | |
| 41 | <span className="shrink-0 font-medium text-warning">{fallbackLabel}</span> | |
| 42 | {approval.appName ? <span className="min-w-0 truncate">{approval.appName}</span> : null} | |
| 43 | {pendingCount > 1 ? ( | |
| 44 | <span className="ml-auto shrink-0 tabular-nums">1/{pendingCount}</span> | |
| 45 | ) : null} | |
| 46 | </span> | |
| 47 | <Detail | |
| 45 | 48 | aria-label={detailAriaLabel} |
| 46 | className="block max-h-20 min-w-0 flex-1 overflow-auto whitespace-pre font-mono text-[11px] text-foreground/85 [scrollbar-width:thin] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70 [&::-webkit-scrollbar]:h-1.5" | |
| 49 | className={cn( | |
| 50 | "block max-h-20 w-full min-w-0 overflow-auto text-xs text-foreground [scrollbar-width:thin] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70 [&::-webkit-scrollbar]:h-1.5", | |
| 51 | approval.requestKind === "mcp-elicitation" | |
| 52 | ? "whitespace-pre-wrap font-sans wrap-break-word" | |
| 53 | : "whitespace-pre font-mono", | |
| 54 | )} | |
| 47 | 55 | data-approval-detail="complete" |
| 48 | 56 | tabIndex={0} |
| 49 | 57 | > |
| 50 | 58 | {approval.detail || fallbackLabel} |
| 51 | </code> | |
| 52 | {pendingCount > 1 ? ( | |
| 53 | <span className="shrink-0 text-[10px] font-medium text-muted-foreground tabular-nums"> | |
| 54 | 1/{pendingCount} | |
| 55 | </span> | |
| 56 | ) : null} | |
| 59 | </Detail> | |
| 57 | 60 | </span> |
| 58 | 61 | ); |
| 59 | 62 | }); |
apps/web/src/components/ui/menu.tsx+8-1| ... | ... | @@ -5,6 +5,7 @@ import { CheckIcon, ChevronRightIcon } from "lucide-react"; |
| 5 | 5 | import type * as React from "react"; |
| 6 | 6 | |
| 7 | 7 | import { cn } from "~/lib/utils"; |
| 8 | import { buttonVariants } from "./button"; | |
| 8 | 9 | |
| 9 | 10 | const MenuCreateHandle = MenuPrimitive.createHandle; |
| 10 | 11 | |
| ... | ... | @@ -81,12 +82,18 @@ function MenuItem({ |
| 81 | 82 | ...props |
| 82 | 83 | }: MenuPrimitive.Item.Props & { |
| 83 | 84 | inset?: boolean; |
| 84 | variant?: "default" | "destructive"; | |
| 85 | variant?: "default" | "destructive" | "ghost"; | |
| 85 | 86 | }) { |
| 86 | 87 | return ( |
| 87 | 88 | <MenuPrimitive.Item |
| 88 | 89 | className={cn( |
| 89 | 90 | "[&>svg]:-mx-0.5 flex min-h-8 cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1 text-base text-foreground outline-none data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-highlighted:bg-accent data-inset:ps-8 data-[variant=destructive]:text-destructive-foreground data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&>svg:not([class*='opacity-'])]:opacity-80 [&>svg:not([class*='size-'])]:size-4.5 sm:[&>svg:not([class*='size-'])]:size-4 [&>svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:[&>svg:not([class*='text-'])]:text-current [&>svg]:pointer-events-none [&>svg]:shrink-0", |
| 91 | variant === "ghost" && | |
| 92 | buttonVariants({ | |
| 93 | variant: "ghost", | |
| 94 | size: "compact", | |
| 95 | className: "h-auto min-h-7 w-full sm:text-xs", | |
| 96 | }), | |
| 90 | 97 | className, |
| 91 | 98 | )} |
| 92 | 99 | data-inset={inset} |