Studio: polish Bypass permissions toggle and add it to chat menu settings (#6442)

* Studio: polish Bypass permissions toggle and add it to chat menu settings

- Recolor the pill, menu item and settings caption from alarm red to a
  bright yellow accent via a new --bypass token (light and dark).
- Use the shield icon on the composer pill (X on hover, like other pills).
- Expand the composer to two rows the moment Bypass permissions is on.
- Keep Bypass leftmost among tool pills; Compare still sits ahead of it.
- Add a Bypass permissions row to Settings > Chat menu so it can be pinned.
- Widen the "More" submenu so the label fits on one line.

* Studio: use shield-ban icon and lighten the Bypass permissions pill

- Swap the lucide shield-off icon for the Hugeicons shield-ban across the
  pill, menu item and settings row.
- Lighten the pill background and deepen the yellow text a touch so the
  label stays readable on a near-white tint.

* Studio: drop the Bypass permissions resting fill, keep it on hover

Show only the yellow icon and label at rest; the rounded hover pill picks
up the yellow accent like the other toggles.

* Studio: lighten the Bypass permissions hover fill

Drop the hover tint so the yellow label stays readable on hover.

---------

Co-authored-by: Unsloth <michaelhan@Michaels-MacBook-Pro.local>
This commit is contained in:
Michael Han 2026-06-18 08:53:38 -07:00 committed by GitHub
commit ecbb64b492
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 82 additions and 36 deletions

View file

@ -125,6 +125,7 @@ import {
Image03Icon,
McpServerIcon,
PencilRulerIcon,
ShieldBanIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { useNavigate } from "@tanstack/react-router";
@ -1247,6 +1248,7 @@ const Composer: FC<{
const artifactsEnabled = useChatRuntimeStore((s) => s.artifactsEnabled);
const mcpEnabledForChat = useChatRuntimeStore((s) => s.mcpEnabledForChat);
const ragEnabled = useChatRuntimeStore((s) => s.ragEnabled);
const bypassPermissions = useChatRuntimeStore((s) => s.bypassPermissions);
// More than 4 pills: collapse to icons only. Search and Code always show;
// Images, RAG, Canvas and MCP are conditional.
const pillsCompact =
@ -1368,6 +1370,8 @@ const Composer: FC<{
}, [composerText, draftKey]);
// Two-row layout shows once the input wraps or a tool is on. Tools can
// pre-select before a model loads, so an active toggle expands it either way.
// Bypass permissions counts too: turning it on should drop the composer into
// the two-row layout immediately, same as Search/Code.
const composerExpanded =
isMultiline ||
hasAttachments ||
@ -1377,7 +1381,8 @@ const Composer: FC<{
imageToolsEnabled ||
ragEnabled ||
artifactsEnabled ||
mcpEnabledForChat;
mcpEnabledForChat ||
bypassPermissions;
// react-textarea-autosize re-measures only on value change or window resize,
// not on the width swap from expanding, so it keeps the taller height and
// leaves a stray blank row. Nudge a resize whenever input width changes.
@ -2430,7 +2435,7 @@ const ArtifactsToggle: FC = () => {
);
};
// Red pill shown while Bypass Permissions is on; click to turn it off.
// Claude gold pill shown while Bypass permissions is on; click to turn it off.
// Mirror of shared-composer's badge so both composers surface the state.
const BypassPermissionsToggle: FC = () => {
const bypassPermissions = useChatRuntimeStore((s) => s.bypassPermissions);
@ -2445,11 +2450,17 @@ const BypassPermissionsToggle: FC = () => {
className="composer-pill-btn"
data-active="true"
data-variant="danger"
aria-label="Disable Bypass Permissions"
title="Bypass Permissions is on (no confirmation, no sandbox). Click to turn off."
aria-label="Disable Bypass permissions"
title="Bypass permissions is on (no confirmation, no sandbox). Click to turn off."
>
<XIcon className="size-3" />
<span>Bypass Permissions</span>
<PillGlyph>
<HugeiconsIcon
icon={ShieldBanIcon}
strokeWidth={2}
className="size-[15px]"
/>
</PillGlyph>
<span>Bypass permissions</span>
</button>
);
};
@ -2959,7 +2970,7 @@ const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({
<MoreHorizontalIcon className="size-4" />
More
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="unsloth-plus-menu w-[232px]">
<DropdownMenuSubContent className="unsloth-plus-menu w-[248px]">
{overflowPlusItems.map((id) => (
<Fragment key={id}>{plusMenuNodes[id]}</Fragment>
))}

View file

@ -1,9 +1,9 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import { ShieldOffIcon } from "lucide-react";
import { useState } from "react";
import { ShieldBanIcon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import {
@ -35,7 +35,7 @@ export function BypassPermissionsMenuItem() {
<>
<DropdownMenuItem
className={
bypassPermissions ? "text-destructive font-medium" : undefined
bypassPermissions ? "text-bypass font-medium" : undefined
}
onSelect={(e) => {
if (bypassPermissions) {
@ -46,8 +46,8 @@ export function BypassPermissionsMenuItem() {
}
}}
>
<ShieldOffIcon />
Bypass Permissions
<HugeiconsIcon icon={ShieldBanIcon} strokeWidth={2} />
Bypass permissions
{bypassPermissions ? (
<HugeiconsIcon icon={Tick02Icon} strokeWidth={2} className="ml-auto" />
) : null}
@ -55,9 +55,9 @@ export function BypassPermissionsMenuItem() {
<AlertDialog open={dialogOpen} onOpenChange={setDialogOpen}>
<AlertDialogContent size="sm">
<AlertDialogHeader>
<AlertDialogTitle>Enable Bypass Permissions?</AlertDialogTitle>
<AlertDialogTitle>Enable Bypass permissions?</AlertDialogTitle>
<AlertDialogDescription>
Bypass Permissions is dangerous since the AI model might delete,
Bypass permissions is dangerous since the AI model might delete,
corrupt your machine, and or cause real world damage to you or the
world - only accept if you are certain
</AlertDialogDescription>

View file

@ -1762,7 +1762,7 @@ function ConfirmToolCallsToggle() {
</div>
{bypassPermissions ? (
<span className="text-[11px] text-muted-foreground">
Overridden by Bypass Permissions
Overridden by Bypass permissions
</span>
) : null}
</div>
@ -1788,7 +1788,7 @@ function BypassPermissionsToggle() {
<div className="flex items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-1.5">
<span className="min-w-0 text-[13px] font-medium leading-[1.25] tracking-nav text-nav-fg">
Bypass Permissions
Bypass permissions
</span>
<InfoHint>
Dangerous. Runs every tool call with no confirmation and disables
@ -1806,16 +1806,16 @@ function BypassPermissionsToggle() {
/>
</div>
{bypassPermissions ? (
<span className="text-[11px] text-destructive">
<span className="text-[11px] text-bypass">
Tool calls run with no confirmation and no sandbox.
</span>
) : null}
<AlertDialog open={dialogOpen} onOpenChange={setDialogOpen}>
<AlertDialogContent size="sm">
<AlertDialogHeader>
<AlertDialogTitle>Enable Bypass Permissions?</AlertDialogTitle>
<AlertDialogTitle>Enable Bypass permissions?</AlertDialogTitle>
<AlertDialogDescription>
Bypass Permissions is dangerous since the AI model might delete,
Bypass permissions is dangerous since the AI model might delete,
corrupt your machine, and or cause real world damage to you or the
world - only accept if you are certain
</AlertDialogDescription>

View file

@ -47,6 +47,7 @@ import {
Image03Icon,
McpServerIcon,
PencilRulerIcon,
ShieldBanIcon,
} from "@hugeicons/core-free-icons";
import { useNavigate } from "@tanstack/react-router";
import { HugeiconsIcon } from "@hugeicons/react";
@ -1593,7 +1594,7 @@ export function SharedComposer({
<MoreHorizontalIcon className="size-4" />
More
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="unsloth-plus-menu w-[232px]">
<DropdownMenuSubContent className="unsloth-plus-menu w-[248px]">
{overflowPlusItems.map((id) => (
<Fragment key={id}>{plusMenuNodes[id]}</Fragment>
))}
@ -1616,6 +1617,29 @@ export function SharedComposer({
</PillGlyph>
<span>Compare</span>
</button>
{/* Bypass sits immediately after Compare and ahead of every other
tool pill (Search, Code, ...) so the active danger state reads
first; only Compare outranks it. */}
{bypassPermissions && (
<button
type="button"
onClick={() => setBypassPermissions(false)}
className="composer-pill-btn"
data-active="true"
data-variant="danger"
aria-label="Disable Bypass permissions"
title="Bypass permissions is on (no confirmation, no sandbox). Click to turn off."
>
<PillGlyph>
<HugeiconsIcon
icon={ShieldBanIcon}
strokeWidth={2}
className="size-[15px]"
/>
</PillGlyph>
<span>Bypass permissions</span>
</button>
)}
<button
type="button"
disabled={searchDisabled}
@ -1731,20 +1755,6 @@ export function SharedComposer({
</button>
) : null}
{mcpEnabledForChat ? <McpComposerButton side="top" /> : null}
{bypassPermissions && (
<button
type="button"
onClick={() => setBypassPermissions(false)}
className="composer-pill-btn"
data-active="true"
data-variant="danger"
aria-label="Disable Bypass Permissions"
title="Bypass Permissions is on (no confirmation, no sandbox). Click to turn off."
>
<XIcon className="size-3" />
<span>Bypass Permissions</span>
</button>
)}
</div>
{/* mr-0.5 matches the send button inset from the edge in normal chat;
gap-1.5 matches its control spacing. */}

View file

@ -43,6 +43,7 @@ import {
Folder01Icon,
McpServerIcon,
PencilRulerIcon,
ShieldBanIcon,
Upload01Icon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
@ -130,6 +131,17 @@ const PLUS_MENU_SETTINGS: { id: PlusMenuItemId; label: string; icon: ReactNode }
/>
),
},
{
id: "bypassPermissions",
label: "Bypass permissions",
icon: (
<HugeiconsIcon
icon={ShieldBanIcon}
strokeWidth={2}
className={PLUS_MENU_ICON_CLASS}
/>
),
},
];
export function ChatTab() {

View file

@ -129,6 +129,8 @@
--accent: oklch(0.9596 0.0275 167.8295);
--accent-foreground: oklch(0.2868 0.0649 159.9823);
--destructive: oklch(0.6368 0.2078 25.3313);
/* Bypass permissions accent: bright, saturated yellow (not dark gold). */
--bypass: #c99d00;
--border: oklch(0.9208 0.0101 164.8536);
--input: oklch(0.9208 0.0101 164.8536);
--ring: #17b88b;
@ -251,6 +253,8 @@
--accent: #3a3d43;
--accent-foreground: #ececee;
--destructive: oklch(0.6368 0.2078 25.3313);
/* Bypass permissions accent: bright, saturated neon yellow on dark. */
--bypass: #ffd60a;
/* --border / --input one step lighter than --muted so outlines and form
borders stay visible on muted surfaces (right config panel, export
tiles, quant chips) and keep subtle contrast on card. */
@ -344,6 +348,7 @@
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-bypass: var(--bypass);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
@ -1047,10 +1052,18 @@
.composer-pill-btn[data-active="true"] {
color: var(--primary);
}
/* Bypass Permissions badge: red, always-visible active warning. */
/* Bypass permissions badge: bright yellow text, no resting fill; the
rounded hover pill picks up the yellow accent like other toggles. */
.composer-pill-btn[data-variant="danger"] {
@apply bg-destructive/10 hover:bg-destructive/15;
color: var(--destructive);
background-color: transparent;
color: var(--bypass);
}
.composer-pill-btn[data-variant="danger"]:hover {
background-color: color-mix(in oklab, var(--bypass) 7%, transparent);
}
/* Match the hover-X circle to the yellow accent instead of the green primary. */
.composer-pill-btn[data-variant="danger"] .composer-pill-x {
background-color: color-mix(in oklab, var(--bypass) 13%, transparent);
}
/* With more than 4 tools on, drop pill labels to icons only to cut clutter.